1// SPDX-License-Identifier: MIT
2
3package template
4
5import (
6 "fmt"
7 "sort"
8 "strings"
9
10 "bnema.dev/ermite-actions/expr"
11)
12
13// ReadYAML parses a YAML document and reads it as a template of the schema
14// definition rootType (for workflows, "workflow-root-strict"). Problems
15// found in the document, such as syntax errors, unexpected keys or
16// misplaced expressions, are recorded in ctx; the returned token holds what
17// could be read and is nil when the document is not valid YAML.
18//
19// An empty document is an error, as in the reference. A document larger than
20// [MaxFileBytes] is refused before it is parsed, with one error about the
21// file.
22func ReadYAML(ctx *Context, rootType, file string, content []byte) (root Token) {
23 if len(content) > MaxFileBytes {
24 e := &Error{File: file, fileOnly: true, Raw: fmt.Sprintf("The file is too large: %d bytes, the limit is %d bytes", len(content), MaxFileBytes)}
25 ctx.add(e)
26 return nil
27 }
28 res := readYAMLDocument(file, content, ctx.RunnerYAML)
29 for _, e := range res.errs {
30 ctx.add(e)
31 }
32 if len(res.errs) > 0 {
33 return nil
34 }
35 defer func() {
36 if r := recover(); r != nil {
37 f, ok := r.(fatal)
38 if !ok {
39 panic(r)
40 }
41 ctx.ErrorInFile(file, f.msg)
42 }
43 }()
44 rd := &reader{ctx: ctx, schema: ctx.schema, file: file}
45 if res.root == nil {
46 if ctx.RunnerYAML {
47 // YamlDotNet produces no DocumentStart event for an empty stream.
48 fatalf("Expected document start parse event")
49 }
50 fatalf("Expected a scalar value, a sequence, or a mapping")
51 }
52 def := newDefinitionInfo(ctx.schema, nil, ctx.schema.get(rootType))
53 root = rd.readValue(res.root, def)
54 for _, e := range res.after {
55 ctx.add(e)
56 }
57 return root
58}
59
60type reader struct {
61 ctx *Context
62 schema *Schema
63 file string
64 // noExpressions is set when reading the result of an evaluation, whose
65 // strings are text even if they contain ${{.
66 noExpressions bool
67 // depth is the nesting of the collections being read, see Context.MaxDepth.
68 depth int
69}
70
71// enter counts one more level of nesting. Like TemplateMemory.IncrementDepth
72// it fails when the levels already entered reach the limit, so a limit of 100
73// allows 100 nested collections.
74func (r *reader) enter() {
75 if r.ctx.MaxDepth <= 0 {
76 return
77 }
78 if r.depth >= r.ctx.MaxDepth {
79 fatalf("Maximum object depth exceeded")
80 }
81 r.depth++
82}
83
84// text is the string a literal becomes when the schema wants a string, and
85// the text of a key that is not a string: Token.String, except that a number
86// is shown as the runner's NumberToken shows it in runner mode.
87func (r *reader) text(t Token) string {
88 if n, ok := t.(*Number); ok && r.ctx.RunnerYAML {
89 return DotNetNumberString(n.Value)
90 }
91 return t.String()
92}
93
94// foldKey is the form of a key under which duplicates are detected: the
95// reference upper-cases like JavaScript, the runner compares keys with
96// .NET's OrdinalIgnoreCase.
97func (r *reader) foldKey(s string) string {
98 if r.ctx.RunnerYAML {
99 return expr.OrdinalFold(s)
100 }
101 return strings.ToUpper(s)
102}
103
104func (r *reader) leave() {
105 if r.ctx.MaxDepth > 0 {
106 r.depth--
107 }
108}
109
110// skip walks a value that is not kept (the value of an unexpected key, the
111// items of a collection the schema does not allow) and counts its nesting,
112// as the reference's SkipValue does.
113func (r *reader) skip(n *rnode) {
114 if n == nil {
115 return
116 }
117 if n.fault != "" {
118 fatalf("%s", n.fault)
119 }
120 if r.ctx.MaxDepth <= 0 {
121 return
122 }
123 switch n.kind {
124 case rSeq:
125 r.enter()
126 for _, it := range n.items {
127 r.skip(it)
128 }
129 r.leave()
130 case rMap:
131 r.enter()
132 for i := range n.keys {
133 r.skip(n.keys[i])
134 r.skip(n.vals[i])
135 }
136 r.leave()
137 }
138}
139
140func (r *reader) origin(n *rnode) Origin { return Origin{File: r.file, Pos: n.pos} }
141
142func (r *reader) readValue(n *rnode, def *DefinitionInfo) Token {
143 if n.fault != "" {
144 fatalf("%s", n.fault)
145 }
146 switch n.kind {
147 case rLiteral:
148 scalar := r.parseScalar(n, n.lit, def)
149 return r.validate(scalar, def)
150 case rSeq:
151 r.enter()
152 seq := NewSequence(r.origin(n))
153 defs := def.schema.definitionsOfType(def.Definition, DefSequence)
154 if len(defs) > 0 {
155 itemDef := def.child(defs[0].ItemType)
156 for _, it := range n.items {
157 seq.Items = append(seq.Items, r.readValue(it, itemDef))
158 }
159 } else {
160 r.ctx.ErrorAt(seq, "A sequence was not expected")
161 for _, it := range n.items {
162 r.skip(it)
163 }
164 }
165 seq.def = def
166 r.leave()
167 return seq
168 }
169 r.enter()
170 m := NewMapping(r.origin(n))
171 defs := def.schema.definitionsOfType(def.Definition, DefMapping)
172 if len(defs) > 0 {
173 if len(defs) > 1 || len(defs[0].Properties) > 0 || defs[0].LooseKeyType == "" {
174 r.mappingWithProperties(n, m, def, defs)
175 } else {
176 keyDef := def.child(defs[0].LooseKeyType)
177 valDef := def.child(defs[0].LooseValueType)
178 r.mappingAllLoose(n, m, def, keyDef, valDef, defs[0])
179 }
180 } else {
181 r.ctx.ErrorAt(m, "A mapping was not expected")
182 for i := range n.keys {
183 r.skip(n.keys[i])
184 r.skip(n.vals[i])
185 }
186 }
187 if m.def == nil {
188 m.def = def
189 }
190 r.leave()
191 return m
192}
193
194// key reads the key of a mapping entry; a key that is not a scalar is an
195// error and reads as an empty string.
196func (r *reader) key(n *rnode, i int, def *DefinitionInfo) Token {
197 k := n.keys[i]
198 if k.fault != "" {
199 fatalf("%s", k.fault)
200 }
201 if k.badKey {
202 if r.ctx.RunnerYAML {
203 // The runner's loop over the keys ends at the first one that is not
204 // a literal, and then expects the end of the mapping.
205 fatalf("Expected mapping end")
206 }
207 r.ctx.ErrorAt(NewString(r.origin(k), ""), "Unexpected value ''")
208 }
209 return r.parseScalar(k, k.lit, def)
210}
211
212func (r *reader) mappingWithProperties(n *rnode, m *Mapping, def *DefinitionInfo, defs []*Definition) {
213 var looseKey, looseVal string
214 if defs[0].LooseKeyType != "" {
215 looseKey, looseVal = defs[0].LooseKeyType, defs[0].LooseValueType
216 }
217 var looseKeyDef, looseValDef *DefinitionInfo
218 upperKeys := map[string]bool{}
219 hasExpressionKey := false
220 for i := range n.keys {
221 keyScalar := r.key(n, i, def)
222 val := n.vals[i]
223 if IsExpression(keyScalar) {
224 hasExpressionKey = true
225 if len(def.allowed) > 0 {
226 m.Add(keyScalar, r.readValue(val, def.child("any")))
227 } else {
228 r.ctx.ErrorAt(keyScalar, "A template expression is not allowed in this context")
229 r.skip(val)
230 }
231 continue
232 }
233 nextKey, ok := keyScalar.(*String)
234 if !ok {
235 nextKey = &String{tokenBase: *keyScalar.base(), Value: r.text(keyScalar)}
236 }
237 if nextKey.Value != "" {
238 up := r.foldKey(nextKey.Value)
239 if upperKeys[up] {
240 r.ctx.Errorf(nextKey, "'%s' is already defined", nextKey.Value)
241 r.skip(val)
242 continue
243 }
244 upperKeys[up] = true
245 }
246 var prop *Property
247 prop, defs = matchPropertyAndFilter(defs, nextKey.Value)
248 if prop != nil {
249 if backgroundStepProperties[nextKey.Value] {
250 r.ctx.Errorf(nextKey, "Unexpected value '%s'", nextKey.Value)
251 r.skip(val)
252 continue
253 }
254 nd := def.child(prop.Type)
255 nextKey.def = nd
256 m.Add(nextKey, r.readValue(val, nd))
257 continue
258 }
259 if looseKey != "" {
260 if looseKeyDef == nil {
261 looseKeyDef = def.child(looseKey)
262 looseValDef = def.child(looseVal)
263 }
264 r.validate(nextKey, looseKeyDef)
265 nextKey.def = def.child(defs[0].LooseValueType)
266 m.Add(nextKey, r.readValue(val, looseValDef))
267 continue
268 }
269 r.ctx.Errorf(nextKey, "Unexpected value '%s'", nextKey.Value)
270 r.skip(val)
271 }
272 if len(defs) == 1 {
273 m.def = newDefinitionInfo(def.schema, def, defs[0])
274 }
275 switch {
276 case len(defs) > 1:
277 hits := map[string]int{}
278 for _, d := range defs {
279 for _, k := range d.PropertyNames {
280 hits[k]++
281 }
282 }
283 var props []string
284 for k, c := range hits {
285 if c == 1 && !backgroundStepOnlyProperties[k] {
286 props = append(props, k)
287 }
288 }
289 sort.Strings(props)
290 r.ctx.Errorf(m, "There's not enough info to determine what you meant. Add one of these properties: %s", strings.Join(props, ", "))
291 case len(defs) == 1 && !hasExpressionKey:
292 for _, name := range defs[0].PropertyNames {
293 if defs[0].Properties[name].Required && !upperKeys[r.foldKey(name)] {
294 r.ctx.Errorf(m, "Required property is missing: %s", name)
295 }
296 }
297 }
298}
299
300var (
301 backgroundStepProperties = map[string]bool{"background": true, "wait": true, "wait-all": true, "cancel": true}
302 backgroundStepOnlyProperties = map[string]bool{"wait": true, "wait-all": true, "cancel": true}
303)
304
305func (r *reader) mappingAllLoose(n *rnode, m *Mapping, def, keyDef, valDef *DefinitionInfo, md *Definition) {
306 upperKeys := map[string]bool{}
307 for i := range n.keys {
308 keyScalar := r.key(n, i, def)
309 keyScalar.base().def = keyDef
310 val := n.vals[i]
311 if IsExpression(keyScalar) {
312 if len(def.allowed) > 0 {
313 m.Add(keyScalar, r.readValue(val, valDef))
314 } else {
315 r.ctx.ErrorAt(keyScalar, "A template expression is not allowed in this context")
316 r.skip(val)
317 }
318 continue
319 }
320 nextKey, ok := keyScalar.(*String)
321 if !ok {
322 nextKey = &String{tokenBase: *keyScalar.base(), Value: r.text(keyScalar)}
323 }
324 if nextKey.Value != "" {
325 up := r.foldKey(nextKey.Value)
326 if upperKeys[up] {
327 r.ctx.Errorf(nextKey, "'%s' is already defined", nextKey.Value)
328 r.skip(val)
329 continue
330 }
331 upperKeys[up] = true
332 }
333 r.validate(nextKey, keyDef)
334 nextKey.def = def.child(md.LooseValueType)
335 m.Add(nextKey, r.readValue(val, valDef))
336 }
337}
338
339// validate checks a scalar against the scalar definitions of def. A literal
340// that is not a string may be converted to one.
341func (r *reader) validate(scalar Token, def *DefinitionInfo) Token {
342 switch scalar.Kind() {
343 case KindNull, KindBoolean, KindNumber, KindString:
344 for _, d := range def.schema.scalarDefinitions(def.Definition) {
345 if d.isMatch(scalar) {
346 scalar.base().def = newDefinitionInfo(def.schema, def, d)
347 return scalar
348 }
349 }
350 if scalar.Kind() != KindString {
351 sl := &String{tokenBase: *scalar.base(), Value: r.text(scalar)}
352 for _, d := range def.schema.scalarDefinitions(def.Definition) {
353 if d.isMatch(sl) {
354 sl.def = newDefinitionInfo(def.schema, def, d)
355 return sl
356 }
357 }
358 }
359 r.ctx.ErrorAt(scalar, fmt.Sprintf("Unexpected value '%s'", r.text(scalar)))
360 case KindBasicExpression:
361 if len(def.allowed) == 0 {
362 r.ctx.ErrorAt(scalar, "A template expression is not allowed in this context")
363 }
364 default:
365 r.ctx.ErrorAt(scalar, fmt.Sprintf("Unexpected value '%s'", scalar.String()))
366 }
367 return scalar
368}