blob: c644ff084dbde74cc1e9fb961dc9a60c0ed00a92 [file] [log] [blame]
Marcel van Lohuizen6cb08782020-01-13 18:03:27 +01001// Copyright 2019 CUE Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package jsonschema
16
17// TODO:
18// - replace converter from YAML to CUE to CUE (schema) to CUE.
19// - define OpenAPI definitions als CUE.
20
21import (
22 "fmt"
23 "path"
Marcel van Lohuizen60b4d912020-01-16 16:48:03 +010024 "path/filepath"
Marcel van Lohuizen6cb08782020-01-13 18:03:27 +010025 "sort"
26 "strings"
27
28 "cuelang.org/go/cue"
29 "cuelang.org/go/cue/ast"
30 "cuelang.org/go/cue/errors"
31 "cuelang.org/go/cue/token"
32 "cuelang.org/go/internal"
33)
34
35const rootDefs = "def"
36
Marcel van Lohuizen6cb08782020-01-13 18:03:27 +010037// A decoder converts JSON schema to CUE.
38type decoder struct {
Marcel van Lohuizen47d98702020-01-17 21:51:39 +010039 cfg *Config
40
Marcel van Lohuizen6cb08782020-01-13 18:03:27 +010041 errs errors.Error
42 imports map[string]*ast.Ident
43
44 definitions []ast.Decl
45}
46
47// addImport registers
48func (d *decoder) addImport(pkg string) *ast.Ident {
49 ident, ok := d.imports[pkg]
50 if !ok {
51 ident = ast.NewIdent(pkg)
52 d.imports[pkg] = ident
53 }
54 return ident
55}
56
Marcel van Lohuizen47d98702020-01-17 21:51:39 +010057func (d *decoder) decode(inst *cue.Instance) *ast.File {
Marcel van Lohuizen6cb08782020-01-13 18:03:27 +010058 root := state{decoder: d}
59 expr, state := root.schemaState(inst.Value())
60
61 var a []ast.Decl
62
Marcel van Lohuizen47d98702020-01-17 21:51:39 +010063 filename := d.cfg.ID
Marcel van Lohuizen60b4d912020-01-16 16:48:03 +010064 name := filepath.ToSlash(filename)
Marcel van Lohuizen6cb08782020-01-13 18:03:27 +010065 if state.id != "" {
66 name = strings.Trim(name, "#")
67 }
68 pkgName := path.Base(name)
69 pkgName = pkgName[:len(pkgName)-len(path.Ext(pkgName))]
70 pkg := &ast.Package{Name: ast.NewIdent(pkgName)}
71 state.doc(pkg)
72
73 a = append(a, pkg)
74
75 var imports []string
76 for k := range d.imports {
77 imports = append(imports, k)
78 }
79 sort.Strings(imports)
80
81 if len(imports) > 0 {
82 x := &ast.ImportDecl{}
83 for _, p := range imports {
84 x.Specs = append(x.Specs, ast.NewImport(nil, p))
85 }
86 a = append(a, x)
87 }
88
89 tags := []string{}
90 if state.jsonschema != "" {
91 tags = append(tags, fmt.Sprintf("schema=%q", state.jsonschema))
92 }
93 if state.id != "" {
94 tags = append(tags, fmt.Sprintf("id=%q", state.id))
95 }
96 if len(tags) > 0 {
97 a = append(a, addTag("Schema", "jsonschema", strings.Join(tags, ",")))
98 }
99
100 if state.deprecated {
101 a = append(a, addTag("Schema", "deprecated", ""))
102 }
103
104 f := &ast.Field{
105 Label: ast.NewIdent("Schema"),
106 Value: expr,
107 }
108
109 f.Token = token.ISA
110 a = append(a, f)
111 a = append(a, d.definitions...)
112
113 return &ast.File{Decls: a}
114}
115
116func (d *decoder) errf(n cue.Value, format string, args ...interface{}) ast.Expr {
117 d.warnf(n, format, args...)
118 return &ast.BadExpr{From: n.Pos()}
119}
120
121func (d *decoder) warnf(n cue.Value, format string, args ...interface{}) {
122 d.errs = errors.Append(d.errs, errors.Newf(n.Pos(), format, args...))
123}
124
125func (d *decoder) number(n cue.Value) ast.Expr {
126 return n.Syntax().(ast.Expr)
127}
128
129func (d *decoder) uint(n cue.Value) ast.Expr {
130 _, err := n.Uint64()
131 if err != nil {
132 d.errf(n, "invalid uint")
133 }
134 return n.Syntax().(ast.Expr)
135}
136
137func (d *decoder) bool(n cue.Value) ast.Expr {
138 return n.Syntax().(ast.Expr)
139}
140
141func (d *decoder) boolValue(n cue.Value) bool {
142 x, err := n.Bool()
143 if err != nil {
144 d.errf(n, "invalid bool")
145 }
146 return x
147}
148
149func (d *decoder) string(n cue.Value) ast.Expr {
150 return n.Syntax().(ast.Expr)
151}
152
153func (d *decoder) strValue(n cue.Value) (s string, ok bool) {
154 s, err := n.String()
155 if err != nil {
156 d.errf(n, "invalid string")
157 return "", false
158 }
159 return s, true
160}
161
162// const draftCutoff = 5
163
164type state struct {
165 *decoder
166
167 path []string
168
169 pos cue.Value
170
171 types []cue.Value
172 typeOptional bool
173 kind cue.Kind
174
175 default_ ast.Expr
176 examples []ast.Expr
177 title string
178 description string
179 deprecated bool
180 jsonschema string
181 id string
182
183 conjuncts []ast.Expr
184
185 obj *ast.StructLit
186 closeStruct bool
187 patterns []ast.Expr
188
189 list *ast.ListLit
190}
191
192// finalize constructs a CUE type from the collected constraints.
193func (s *state) finalize() (e ast.Expr) {
194 if s.typeOptional || s.kind != 0 {
195 if len(s.types) > 1 {
196 s.errf(s.pos, "constraints require specific type")
197 }
198 s.types = nil
199 }
200
201 conjuncts := []ast.Expr{}
202 disjuncts := []ast.Expr{}
203 for _, n := range s.types {
204 add := func(e ast.Expr) {
205 disjuncts = append(disjuncts, setPos(e, n))
206 }
207 str, ok := s.strValue(n)
208 if !ok {
209 s.errf(n, "type value should be a string")
210 return
211 }
212 switch str {
213 case "null":
214 // TODO: handle OpenAPI restrictions.
215 add(ast.NewIdent("null"))
216 case "boolean":
217 add(ast.NewIdent("bool"))
218 case "string":
219 add(ast.NewIdent("string"))
220 case "number":
221 add(ast.NewIdent("number"))
222 case "integer":
223 add(ast.NewIdent("int"))
224 case "array":
225 if s.kind&cue.ListKind == 0 {
226 add(ast.NewList(&ast.Ellipsis{}))
227 }
228 case "object":
229 elps := &ast.Ellipsis{}
230 st := &ast.StructLit{Elts: []ast.Decl{elps}}
231 add(st)
232 default:
233 s.errf(n, "unknown type %q", n)
234 }
235 }
236 if len(disjuncts) > 0 {
237 conjuncts = append(conjuncts, ast.NewBinExpr(token.OR, disjuncts...))
238 }
239
240 conjuncts = append(conjuncts, s.conjuncts...)
241
242 if s.obj != nil {
243 if !s.closeStruct {
244 s.obj.Elts = append(s.obj.Elts, &ast.Ellipsis{})
245 }
246 conjuncts = append(conjuncts, s.obj)
247 }
248
249 if len(conjuncts) == 0 {
250 return ast.NewString(fmt.Sprint(s.pos))
251 }
252
253 e = ast.NewBinExpr(token.AND, conjuncts...)
254
255 if s.default_ != nil {
256 // check conditions where default can be skipped.
257 switch x := s.default_.(type) {
258 case *ast.ListLit:
259 if s.kind == cue.ListKind && len(x.Elts) == 0 {
260 return e
261 }
262 }
263 e = ast.NewBinExpr(token.OR, e, &ast.UnaryExpr{Op: token.MUL, X: s.default_})
264 }
265 return e
266}
267
268func (s *state) doc(n ast.Node) {
269 // Create documentation.
270 doc := strings.TrimSpace(s.title)
271 if s.description != "" {
272 if doc != "" {
273 doc += "\n\n"
274 }
275 doc += s.description
276 doc = strings.TrimSpace(doc)
277 }
278 if doc != "" {
279 ast.SetComments(n, []*ast.CommentGroup{
280 internal.NewComment(true, doc),
281 })
282 }
283
284 // TODO: add examples as well?
285}
286
287func (s *state) add(e ast.Expr) {
288 s.conjuncts = append(s.conjuncts, e)
289}
290
291func (s *state) schema(n cue.Value) ast.Expr {
292 expr, _ := s.schemaState(n)
293 // TODO: report unused doc.
294 return expr
295}
296
297func (s *state) schemaState(n cue.Value) (ast.Expr, *state) {
298 state := &state{path: s.path, pos: n, decoder: s.decoder}
299
300 if n.Kind() != cue.StructKind {
301 return s.errf(n, "schema expects mapping node, found %s", n.Kind()), state
302 }
303
304 // do multiple passes over the constraints to ensure they are done in order.
305 for pass := 0; pass < 3; pass++ {
306 state.processMap(n, func(key string, value cue.Value) {
307 // Convert each constraint into a either a value or a functor.
308 c := constraintMap[key]
309 if c == nil {
310 if pass == 0 {
311 s.warnf(n, "unsupported constraint %q", key)
312 }
313 return
314 }
315 if c.phase == pass {
316 c.fn(value, state)
317 }
318 })
319 }
320
321 return state.finalize(), state
322}
323
324func (s *state) value(n cue.Value) ast.Expr {
325 switch n.Kind() {
326 case cue.ListKind:
327 a := []ast.Expr{}
328 for i, _ := n.List(); i.Next(); {
329 a = append(a, s.value(i.Value()))
330 }
331 return setPos(ast.NewList(a...), n)
332
333 case cue.StructKind:
334 a := []ast.Decl{}
335 s.processMap(n, func(key string, n cue.Value) {
336 a = append(a, &ast.Field{
337 Label: ast.NewString(key),
338 Value: s.value(n),
339 })
340 })
341 a = append(a, &ast.Ellipsis{})
342 return setPos(&ast.StructLit{Elts: a}, n)
343
344 default:
345 if !n.IsConcrete() {
346 s.errf(n, "invalid non-concerte value")
347 }
348 return n.Syntax().(ast.Expr)
349 }
350}
351
352// processMap processes a yaml node, expanding merges.
353//
354// TODO: in some cases we can translate merges into CUE embeddings.
355// This may also prevent exponential blow-up (as may happen when
356// converting YAML to JSON).
357func (s *state) processMap(n cue.Value, f func(key string, n cue.Value)) {
358 saved := s.path
359 defer func() { s.path = saved }()
360
361 // TODO: intercept references to allow for optimized performance.
362 for i, _ := n.Fields(); i.Next(); {
363 key := i.Label()
364 s.path = append(saved, key)
365 f(key, i.Value())
366 }
367}
368
369func list(n cue.Value) (a []cue.Value) {
370 for i, _ := n.List(); i.Next(); {
371 a = append(a, i.Value())
372 }
373 return a
374}
375
376// excludeFields returns a CUE expression that can be used to exclude the
377// fields of the given declaration in a label expression. For instance, for
378//
379// { foo: 1, bar: int }
380//
381// it creates
382//
383// "^(foo|bar)$"
384//
385// which can be used in a label expression to define types for all fields but
386// those existing:
387//
388// [!~"^(foo|bar)$"]: string
389//
390func excludeFields(decls []ast.Decl) ast.Expr {
391 var a []string
392 for _, d := range decls {
393 f, ok := d.(*ast.Field)
394 if !ok {
395 continue
396 }
397 str, _, _ := ast.LabelName(f.Label)
398 if str != "" {
399 a = append(a, str)
400 }
401 }
402 re := fmt.Sprintf("^(%s)$", strings.Join(a, "|"))
403 return &ast.UnaryExpr{Op: token.NMAT, X: ast.NewString(re)}
404}
405
406func addTag(field, tag, value string) *ast.Field {
407 return &ast.Field{
408 Label: ast.NewIdent(field),
409 Token: token.ISA,
410 Value: ast.NewIdent("_"),
411 Attrs: []*ast.Attribute{
412 &ast.Attribute{Text: fmt.Sprintf("@%s(%s)", tag, value)},
413 },
414 }
415}
416
417func setPos(e ast.Expr, v cue.Value) ast.Expr {
418 ast.SetPos(e, v.Pos())
419 return e
420}