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}