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}