main 98594d0

schema_load.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package template
4
5import (
6	"bytes"
7	"encoding/json"
8	"errors"
9	"fmt"
10	"io"
11	"strings"
12	"sync"
13
14	_ "embed"
15)
16
17//go:embed schema/workflow-v1.0.min.json
18var workflowSchemaJSON []byte
19
20var (
21	workflowSchemaOnce sync.Once
22	workflowSchema     *Schema
23	workflowSchemaErr  error
24)
25
26// WorkflowSchema returns the vendored workflow schema (workflow-v1.0). The
27// schema is read once and shared; it must not be modified.
28func WorkflowSchema() (*Schema, error) {
29	workflowSchemaOnce.Do(func() {
30		workflowSchema, workflowSchemaErr = ParseSchema(workflowSchemaJSON)
31	})
32	return workflowSchema, workflowSchemaErr
33}
34
35// jnode is a JSON value that keeps the order of object keys.
36type jnode struct {
37	kind byte // 'o'bject, 'a'rray, 's'tring, 'b'ool, 'n'umber, 'z' null
38	keys []string
39	vals []*jnode
40	str  string
41	b    bool
42}
43
44func (n *jnode) get(key string) *jnode {
45	for i, k := range n.keys {
46		if k == key {
47			return n.vals[i]
48		}
49	}
50	return nil
51}
52
53func readJNode(dec *json.Decoder) (*jnode, error) {
54	t, err := dec.Token()
55	if err != nil {
56		return nil, err
57	}
58	switch v := t.(type) {
59	case json.Delim:
60		switch v {
61		case '{':
62			n := &jnode{kind: 'o'}
63			for dec.More() {
64				kt, err := dec.Token()
65				if err != nil {
66					return nil, err
67				}
68				val, err := readJNode(dec)
69				if err != nil {
70					return nil, err
71				}
72				n.keys = append(n.keys, kt.(string))
73				n.vals = append(n.vals, val)
74			}
75			_, err := dec.Token()
76			return n, err
77		case '[':
78			n := &jnode{kind: 'a'}
79			for dec.More() {
80				val, err := readJNode(dec)
81				if err != nil {
82					return nil, err
83				}
84				n.vals = append(n.vals, val)
85			}
86			_, err := dec.Token()
87			return n, err
88		}
89	case string:
90		return &jnode{kind: 's', str: v}, nil
91	case bool:
92		return &jnode{kind: 'b', b: v}, nil
93	case nil:
94		return &jnode{kind: 'z'}, nil
95	case json.Number, float64:
96		return &jnode{kind: 'n'}, nil
97	}
98	return nil, errors.New("unexpected JSON token")
99}
100
101// ParseSchema reads a schema in the JSON form of workflow-v1.0.min.json: an
102// object with "version" and "definitions". The built-in definitions null,
103// boolean, number, string, sequence, mapping and any are always present.
104func ParseSchema(data []byte) (s *Schema, err error) {
105	defer func() {
106		if r := recover(); r != nil {
107			f, ok := r.(fatal)
108			if !ok {
109				panic(r)
110			}
111			s, err = nil, errors.New(f.msg)
112		}
113	}()
114	dec := json.NewDecoder(bytes.NewReader(data))
115	root, err := readJNode(dec)
116	if err != nil {
117		return nil, fmt.Errorf("invalid schema JSON: %w", err)
118	}
119	if _, err := dec.Token(); err != io.EOF {
120		return nil, errors.New("invalid schema JSON: trailing data")
121	}
122	if root.kind != 'o' {
123		fatalf("The schema must be an object")
124	}
125	s = newBuiltinSchema()
126	for i, key := range root.keys {
127		val := root.vals[i]
128		switch key {
129		case "version":
130			if val.kind != 's' || val.str == "" {
131				fatalf("Unexpected value in template-schema version")
132			}
133			s.Version = val.str
134		case "definitions":
135			if val.kind != 'o' {
136				fatalf("template-schema definitions must be an object")
137			}
138			for j, name := range val.keys {
139				s.loadDefinition(name, val.vals[j])
140			}
141		default:
142			fatalf("Error while reading 'template-schema key'. Unexpected value '%s'", key)
143		}
144	}
145	s.validate()
146	return s, nil
147}
148
149func newBuiltinSchema() *Schema {
150	s := &Schema{definitions: map[string]*Definition{}}
151	for _, d := range []*Definition{
152		{Name: "null", Type: DefNull},
153		{Name: "boolean", Type: DefBoolean},
154		{Name: "number", Type: DefNumber},
155		{Name: "string", Type: DefString},
156		{Name: "sequence", Type: DefSequence, ItemType: "any"},
157		{Name: "mapping", Type: DefMapping, LooseKeyType: "string", LooseValueType: "any"},
158		{Name: "any", Type: DefOneOf, OneOf: []string{"null", "boolean", "number", "string", "sequence", "mapping"}},
159	} {
160		s.definitions[d.Name] = d
161	}
162	return s
163}
164
165func (s *Schema) loadDefinition(name string, n *jnode) {
166	if n.kind != 'o' {
167		fatalf("Error while reading 'definitions value'. Unexpected value")
168	}
169	d := &Definition{Name: name}
170	var body *jnode
171	typed := false
172	for i, k := range n.keys {
173		v := n.vals[i]
174		switch k {
175		case "description":
176			d.Description = v.str
177		case "context":
178			seen := map[string]bool{}
179			for _, it := range v.vals {
180				u := strings.ToUpper(it.str)
181				if seen[u] {
182					fatalf("Duplicate context item '%s'", it.str)
183				}
184				seen[u] = true
185				d.Context = append(d.Context, it.str)
186			}
187		case "null", "boolean", "number", "string", "sequence", "mapping", "one-of", "allowed-values":
188			if typed {
189				continue
190			}
191			typed = true
192			body = v
193			d.Type = map[string]DefinitionType{
194				"null": DefNull, "boolean": DefBoolean, "number": DefNumber, "string": DefString,
195				"sequence": DefSequence, "mapping": DefMapping, "one-of": DefOneOf, "allowed-values": DefOneOf,
196			}[k]
197			if k == "allowed-values" {
198				for _, it := range v.vals {
199					key := name + "-" + it.str
200					s.definitions[key] = &Definition{Name: key, Type: DefString, Constant: trimDisplay(it.str)}
201					d.OneOf = append(d.OneOf, key)
202				}
203			}
204		default:
205			fatalf("Error while reading 'definition mapping key'. Unexpected value '%s'", k)
206		}
207	}
208	if !typed {
209		fatalf("Not enough information to construct definition '%s'", name)
210	}
211	switch d.Type {
212	case DefString:
213		for i, k := range body.keys {
214			v := body.vals[i]
215			switch k {
216			case "constant":
217				d.Constant = v.str
218			case "ignore-case":
219				d.IgnoreCase = v.b
220			case "require-non-empty":
221				d.RequireNonEmpty = v.b
222			case "is-expression":
223				d.IsExpression = v.b
224			default:
225				fatalf("Error while reading 'definition string key'. Unexpected value '%s'", k)
226			}
227		}
228	case DefSequence:
229		if t := body.get("item-type"); t != nil {
230			d.ItemType = t.str
231		}
232	case DefMapping:
233		d.Properties = map[string]Property{}
234		for i, k := range body.keys {
235			v := body.vals[i]
236			switch k {
237			case "properties":
238				for j, pn := range v.keys {
239					d.PropertyNames = append(d.PropertyNames, pn)
240					d.Properties[pn] = loadProperty(v.vals[j])
241				}
242			case "loose-key-type":
243				d.LooseKeyType = v.str
244			case "loose-value-type":
245				d.LooseValueType = v.str
246			default:
247				fatalf("Error while reading 'definition mapping key'. Unexpected value '%s'", k)
248			}
249		}
250	case DefOneOf:
251		if body != nil && body.kind == 'a' && len(d.OneOf) == 0 {
252			for _, it := range body.vals {
253				d.OneOf = append(d.OneOf, it.str)
254			}
255		}
256	}
257	s.definitions[name] = d
258}
259
260func loadProperty(n *jnode) Property {
261	if n.kind == 's' {
262		return Property{Type: n.str}
263	}
264	var p Property
265	for i, k := range n.keys {
266		v := n.vals[i]
267		switch k {
268		case "type":
269			p.Type = v.str
270		case "required":
271			p.Required = v.b
272		case "description":
273			p.Description = v.str
274		default:
275			fatalf("Error while reading 'mapping-property-value key'. Unexpected value '%s'", k)
276		}
277	}
278	return p
279}
280
281// validate checks that every reference resolves and the one-of definitions
282// are unambiguous, as TemplateSchema.validate does.
283func (s *Schema) validate() {
284	for name, d := range s.definitions {
285		if !validDefinitionName(name) {
286			fatalf("Invalid definition name '%s'", name)
287		}
288		switch d.Type {
289		case DefString:
290			if d.Constant != "" && d.RequireNonEmpty {
291				fatalf("Properties 'constant' and 'require-non-empty' cannot both be set")
292			}
293		case DefSequence:
294			if d.ItemType == "" {
295				fatalf("'%s' does not defined 'item-type'", name)
296			}
297			s.get(d.ItemType)
298		case DefMapping:
299			switch {
300			case d.LooseKeyType != "":
301				s.get(d.LooseKeyType)
302				if d.LooseValueType == "" {
303					fatalf("Property 'loose-key-type' is defined but 'loose-value-type' is not defined on '%s'", name)
304				}
305				s.get(d.LooseValueType)
306			case d.LooseValueType != "":
307				fatalf("Property 'loose-value-type' is defined but 'loose-key-type' is not defined on '%s'", name)
308			}
309			for pn, p := range d.Properties {
310				if p.Type == "" {
311					fatalf("Type not specified for the property '%s' on '%s'", pn, name)
312				}
313				s.get(p.Type)
314			}
315		case DefOneOf:
316			if len(d.OneOf) == 0 {
317				fatalf("'%s' does not contain any references", name)
318			}
319			seen := map[string]bool{}
320			for _, n := range d.OneOf {
321				if seen[n] {
322					fatalf("'%s' contains duplicate nested type '%s'", name, n)
323				}
324				seen[n] = true
325				if len(s.get(n).Context) > 0 {
326					fatalf("'%s' is a one-of definition and references another definition that defines context. This is currently not supported.", name)
327				}
328			}
329		}
330	}
331}
332
333func validDefinitionName(n string) bool {
334	if n == "" {
335		return false
336	}
337	for i, r := range n {
338		switch {
339		case r == '_' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z':
340		case i > 0 && (r == '-' || r >= '0' && r <= '9'):
341		default:
342			return false
343		}
344	}
345	return true
346}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close