v0.1.0 28f9d6e

reader.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package template
4
5import (
6	"fmt"
7	"sort"
8	"strings"
9)
10
11// ReadYAML parses a YAML document and reads it as a template of the schema
12// definition rootType (for workflows, "workflow-root-strict"). Problems
13// found in the document, such as syntax errors, unexpected keys or
14// misplaced expressions, are recorded in ctx; the returned token holds what
15// could be read and is nil when the document is not valid YAML.
16//
17// An empty document is an error, as in the reference. A document larger than
18// [MaxFileBytes] is refused before it is parsed, with one error about the
19// file.
20func ReadYAML(ctx *Context, rootType, file string, content []byte) (root Token) {
21	if len(content) > MaxFileBytes {
22		e := &Error{File: file, fileOnly: true, Raw: fmt.Sprintf("The file is too large: %d bytes, the limit is %d bytes", len(content), MaxFileBytes)}
23		ctx.add(e)
24		return nil
25	}
26	res := readYAMLDocument(file, content)
27	for _, e := range res.errs {
28		ctx.add(e)
29	}
30	if len(res.errs) > 0 {
31		return nil
32	}
33	defer func() {
34		if r := recover(); r != nil {
35			f, ok := r.(fatal)
36			if !ok {
37				panic(r)
38			}
39			ctx.ErrorInFile(file, f.msg)
40		}
41	}()
42	rd := &reader{ctx: ctx, schema: ctx.schema, file: file}
43	if res.root == nil {
44		fatalf("Expected a scalar value, a sequence, or a mapping")
45	}
46	def := newDefinitionInfo(ctx.schema, nil, ctx.schema.get(rootType))
47	return rd.readValue(res.root, def)
48}
49
50type reader struct {
51	ctx    *Context
52	schema *Schema
53	file   string
54	// noExpressions is set when reading the result of an evaluation, whose
55	// strings are text even if they contain ${{.
56	noExpressions bool
57}
58
59func (r *reader) origin(n *rnode) Origin { return Origin{File: r.file, Pos: n.pos} }
60
61func (r *reader) readValue(n *rnode, def *DefinitionInfo) Token {
62	switch n.kind {
63	case rLiteral:
64		scalar := r.parseScalar(n, n.lit, def)
65		return r.validate(scalar, def)
66	case rSeq:
67		seq := NewSequence(r.origin(n))
68		defs := def.schema.definitionsOfType(def.Definition, DefSequence)
69		if len(defs) > 0 {
70			itemDef := def.child(defs[0].ItemType)
71			for _, it := range n.items {
72				seq.Items = append(seq.Items, r.readValue(it, itemDef))
73			}
74		} else {
75			r.ctx.ErrorAt(seq, "A sequence was not expected")
76		}
77		seq.def = def
78		return seq
79	}
80	m := NewMapping(r.origin(n))
81	defs := def.schema.definitionsOfType(def.Definition, DefMapping)
82	if len(defs) > 0 {
83		if len(defs) > 1 || len(defs[0].Properties) > 0 || defs[0].LooseKeyType == "" {
84			r.mappingWithProperties(n, m, def, defs)
85		} else {
86			keyDef := def.child(defs[0].LooseKeyType)
87			valDef := def.child(defs[0].LooseValueType)
88			r.mappingAllLoose(n, m, def, keyDef, valDef, defs[0])
89		}
90	} else {
91		r.ctx.ErrorAt(m, "A mapping was not expected")
92	}
93	if m.def == nil {
94		m.def = def
95	}
96	return m
97}
98
99// key reads the key of a mapping entry; a key that is not a scalar is an
100// error and reads as an empty string.
101func (r *reader) key(n *rnode, i int, def *DefinitionInfo) Token {
102	k := n.keys[i]
103	if k.badKey {
104		r.ctx.ErrorAt(NewString(r.origin(k), ""), "Unexpected value ''")
105	}
106	return r.parseScalar(k, k.lit, def)
107}
108
109func (r *reader) mappingWithProperties(n *rnode, m *Mapping, def *DefinitionInfo, defs []*Definition) {
110	var looseKey, looseVal string
111	if defs[0].LooseKeyType != "" {
112		looseKey, looseVal = defs[0].LooseKeyType, defs[0].LooseValueType
113	}
114	var looseKeyDef, looseValDef *DefinitionInfo
115	upperKeys := map[string]bool{}
116	hasExpressionKey := false
117	for i := range n.keys {
118		keyScalar := r.key(n, i, def)
119		val := n.vals[i]
120		if IsExpression(keyScalar) {
121			hasExpressionKey = true
122			if len(def.allowed) > 0 {
123				m.Add(keyScalar, r.readValue(val, def.child("any")))
124			} else {
125				r.ctx.ErrorAt(keyScalar, "A template expression is not allowed in this context")
126			}
127			continue
128		}
129		nextKey, ok := keyScalar.(*String)
130		if !ok {
131			nextKey = &String{tokenBase: *keyScalar.base(), Value: keyScalar.String()}
132		}
133		if nextKey.Value != "" {
134			up := strings.ToUpper(nextKey.Value)
135			if upperKeys[up] {
136				r.ctx.Errorf(nextKey, "'%s' is already defined", nextKey.Value)
137				continue
138			}
139			upperKeys[up] = true
140		}
141		var prop *Property
142		prop, defs = matchPropertyAndFilter(defs, nextKey.Value)
143		if prop != nil {
144			if backgroundStepProperties[nextKey.Value] {
145				r.ctx.Errorf(nextKey, "Unexpected value '%s'", nextKey.Value)
146				continue
147			}
148			nd := def.child(prop.Type)
149			nextKey.def = nd
150			m.Add(nextKey, r.readValue(val, nd))
151			continue
152		}
153		if looseKey != "" {
154			if looseKeyDef == nil {
155				looseKeyDef = def.child(looseKey)
156				looseValDef = def.child(looseVal)
157			}
158			r.validate(nextKey, looseKeyDef)
159			nextKey.def = def.child(defs[0].LooseValueType)
160			m.Add(nextKey, r.readValue(val, looseValDef))
161			continue
162		}
163		r.ctx.Errorf(nextKey, "Unexpected value '%s'", nextKey.Value)
164	}
165	if len(defs) == 1 {
166		m.def = newDefinitionInfo(def.schema, def, defs[0])
167	}
168	switch {
169	case len(defs) > 1:
170		hits := map[string]int{}
171		for _, d := range defs {
172			for _, k := range d.PropertyNames {
173				hits[k]++
174			}
175		}
176		var props []string
177		for k, c := range hits {
178			if c == 1 && !backgroundStepOnlyProperties[k] {
179				props = append(props, k)
180			}
181		}
182		sort.Strings(props)
183		r.ctx.Errorf(m, "There's not enough info to determine what you meant. Add one of these properties: %s", strings.Join(props, ", "))
184	case len(defs) == 1 && !hasExpressionKey:
185		for _, name := range defs[0].PropertyNames {
186			if defs[0].Properties[name].Required && !upperKeys[strings.ToUpper(name)] {
187				r.ctx.Errorf(m, "Required property is missing: %s", name)
188			}
189		}
190	}
191}
192
193var (
194	backgroundStepProperties     = map[string]bool{"background": true, "wait": true, "wait-all": true, "cancel": true}
195	backgroundStepOnlyProperties = map[string]bool{"wait": true, "wait-all": true, "cancel": true}
196)
197
198func (r *reader) mappingAllLoose(n *rnode, m *Mapping, def, keyDef, valDef *DefinitionInfo, md *Definition) {
199	upperKeys := map[string]bool{}
200	for i := range n.keys {
201		keyScalar := r.key(n, i, def)
202		keyScalar.base().def = keyDef
203		val := n.vals[i]
204		if IsExpression(keyScalar) {
205			if len(def.allowed) > 0 {
206				m.Add(keyScalar, r.readValue(val, valDef))
207			} else {
208				r.ctx.ErrorAt(keyScalar, "A template expression is not allowed in this context")
209			}
210			continue
211		}
212		nextKey, ok := keyScalar.(*String)
213		if !ok {
214			nextKey = &String{tokenBase: *keyScalar.base(), Value: keyScalar.String()}
215		}
216		if nextKey.Value != "" {
217			up := strings.ToUpper(nextKey.Value)
218			if upperKeys[up] {
219				r.ctx.Errorf(nextKey, "'%s' is already defined", nextKey.Value)
220				continue
221			}
222			upperKeys[up] = true
223		}
224		r.validate(nextKey, keyDef)
225		nextKey.def = def.child(md.LooseValueType)
226		m.Add(nextKey, r.readValue(val, valDef))
227	}
228}
229
230// validate checks a scalar against the scalar definitions of def. A literal
231// that is not a string may be converted to one.
232func (r *reader) validate(scalar Token, def *DefinitionInfo) Token {
233	switch scalar.Kind() {
234	case KindNull, KindBoolean, KindNumber, KindString:
235		for _, d := range def.schema.scalarDefinitions(def.Definition) {
236			if d.isMatch(scalar) {
237				scalar.base().def = newDefinitionInfo(def.schema, def, d)
238				return scalar
239			}
240		}
241		if scalar.Kind() != KindString {
242			sl := &String{tokenBase: *scalar.base(), Value: scalar.String()}
243			for _, d := range def.schema.scalarDefinitions(def.Definition) {
244				if d.isMatch(sl) {
245					sl.def = newDefinitionInfo(def.schema, def, d)
246					return sl
247				}
248			}
249		}
250		r.ctx.ErrorAt(scalar, fmt.Sprintf("Unexpected value '%s'", scalar.String()))
251	case KindBasicExpression:
252		if len(def.allowed) == 0 {
253			r.ctx.ErrorAt(scalar, "A template expression is not allowed in this context")
254		}
255	default:
256		r.ctx.ErrorAt(scalar, fmt.Sprintf("Unexpected value '%s'", scalar.String()))
257	}
258	return scalar
259}

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