cuelang.org/go@v0.10.1/encoding/jsonschema/decode.go (about) 1 // 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 15 package jsonschema 16 17 // TODO: 18 // - replace converter from YAML to CUE to CUE (schema) to CUE. 19 // - define OpenAPI definitions als CUE. 20 21 import ( 22 "fmt" 23 "net/url" 24 "sort" 25 "strings" 26 27 "cuelang.org/go/cue" 28 "cuelang.org/go/cue/ast" 29 "cuelang.org/go/cue/ast/astutil" 30 "cuelang.org/go/cue/errors" 31 "cuelang.org/go/cue/token" 32 "cuelang.org/go/internal" 33 ) 34 35 // rootDefs defines the top-level name of the map of definitions that do not 36 // have a valid identifier name. 37 // 38 // TODO: find something more principled, like allowing #."a-b" or `#a-b`. 39 const rootDefs = "#" 40 41 // A decoder converts JSON schema to CUE. 42 type decoder struct { 43 cfg *Config 44 errs errors.Error 45 numID int // for creating unique numbers: increment on each use 46 mapURLErrors map[string]bool 47 } 48 49 // addImport registers 50 func (d *decoder) addImport(n cue.Value, pkg string) *ast.Ident { 51 spec := ast.NewImport(nil, pkg) 52 info, err := astutil.ParseImportSpec(spec) 53 if err != nil { 54 d.errf(cue.Value{}, "invalid import %q", pkg) 55 } 56 ident := ast.NewIdent(info.Ident) 57 ident.Node = spec 58 ast.SetPos(ident, n.Pos()) 59 60 return ident 61 } 62 63 func (d *decoder) decode(v cue.Value) *ast.File { 64 f := &ast.File{} 65 66 if pkgName := d.cfg.PkgName; pkgName != "" { 67 pkg := &ast.Package{Name: ast.NewIdent(pkgName)} 68 f.Decls = append(f.Decls, pkg) 69 } 70 71 var a []ast.Decl 72 73 if d.cfg.Root == "" { 74 a = append(a, d.schema(nil, v)...) 75 } else { 76 ref := d.parseRef(token.NoPos, d.cfg.Root) 77 if ref == nil { 78 return f 79 } 80 var selectors []cue.Selector 81 for _, r := range ref { 82 selectors = append(selectors, cue.Str(r)) 83 } 84 i, err := v.LookupPath(cue.MakePath(selectors...)).Fields() 85 if err != nil { 86 d.errs = errors.Append(d.errs, errors.Promote(err, "")) 87 return nil 88 } 89 for i.Next() { 90 ref := append(ref, i.Label()) 91 lab := d.mapRef(i.Value().Pos(), "", ref) 92 if len(lab) == 0 { 93 return nil 94 } 95 decls := d.schema(lab, i.Value()) 96 a = append(a, decls...) 97 } 98 } 99 100 f.Decls = append(f.Decls, a...) 101 102 _ = astutil.Sanitize(f) 103 104 return f 105 } 106 107 func (d *decoder) schema(ref []ast.Label, v cue.Value) (a []ast.Decl) { 108 root := state{decoder: d} 109 110 var name ast.Label 111 inner := len(ref) - 1 112 113 if inner >= 0 { 114 name = ref[inner] 115 root.isSchema = true 116 } 117 118 expr, state := root.schemaState(v, allTypes, nil, false) 119 120 tags := []string{} 121 if state.schemaVersionPresent { 122 // TODO use cue/literal.String 123 tags = append(tags, fmt.Sprintf("schema=%q", state.schemaVersion)) 124 } 125 126 if name == nil { 127 if len(tags) > 0 { 128 body := strings.Join(tags, ",") 129 a = append(a, &ast.Attribute{ 130 Text: fmt.Sprintf("@jsonschema(%s)", body)}) 131 } 132 133 if state.deprecated { 134 a = append(a, &ast.Attribute{Text: "@deprecated()"}) 135 } 136 } else { 137 if len(tags) > 0 { 138 a = append(a, addTag(name, "jsonschema", strings.Join(tags, ","))) 139 } 140 141 if state.deprecated { 142 a = append(a, addTag(name, "deprecated", "")) 143 } 144 } 145 146 if name != nil { 147 f := &ast.Field{ 148 Label: name, 149 Value: expr, 150 } 151 152 a = append(a, f) 153 } else if st, ok := expr.(*ast.StructLit); ok { 154 a = append(a, st.Elts...) 155 } else { 156 a = append(a, &ast.EmbedDecl{Expr: expr}) 157 } 158 159 state.doc(a[0]) 160 161 for i := inner - 1; i >= 0; i-- { 162 a = []ast.Decl{&ast.Field{ 163 Label: ref[i], 164 Value: &ast.StructLit{Elts: a}, 165 }} 166 expr = ast.NewStruct(ref[i], expr) 167 } 168 169 if root.hasSelfReference { 170 return []ast.Decl{ 171 &ast.EmbedDecl{Expr: ast.NewIdent(topSchema)}, 172 &ast.Field{ 173 Label: ast.NewIdent(topSchema), 174 Value: &ast.StructLit{Elts: a}, 175 }, 176 } 177 } 178 179 return a 180 } 181 182 func (d *decoder) errf(n cue.Value, format string, args ...interface{}) ast.Expr { 183 d.warnf(n.Pos(), format, args...) 184 return &ast.BadExpr{From: n.Pos()} 185 } 186 187 func (d *decoder) warnf(p token.Pos, format string, args ...interface{}) { 188 d.addErr(errors.Newf(p, format, args...)) 189 } 190 191 func (d *decoder) addErr(err errors.Error) { 192 d.errs = errors.Append(d.errs, err) 193 } 194 195 func (d *decoder) number(n cue.Value) ast.Expr { 196 return n.Syntax(cue.Final()).(ast.Expr) 197 } 198 199 func (d *decoder) uint(n cue.Value) ast.Expr { 200 _, err := n.Uint64() 201 if err != nil { 202 d.errf(n, "invalid uint") 203 } 204 return n.Syntax(cue.Final()).(ast.Expr) 205 } 206 207 func (d *decoder) boolValue(n cue.Value) bool { 208 x, err := n.Bool() 209 if err != nil { 210 d.errf(n, "invalid bool") 211 } 212 return x 213 } 214 215 func (d *decoder) string(n cue.Value) ast.Expr { 216 return n.Syntax(cue.Final()).(ast.Expr) 217 } 218 219 func (d *decoder) strValue(n cue.Value) (s string, ok bool) { 220 s, err := n.String() 221 if err != nil { 222 d.errf(n, "invalid string") 223 return "", false 224 } 225 return s, true 226 } 227 228 // const draftCutoff = 5 229 230 type coreType int 231 232 const ( 233 nullType coreType = iota 234 boolType 235 numType 236 stringType 237 arrayType 238 objectType 239 240 numCoreTypes 241 ) 242 243 var coreToCUE = []cue.Kind{ 244 nullType: cue.NullKind, 245 boolType: cue.BoolKind, 246 numType: cue.NumberKind, // Note: both int and float. 247 stringType: cue.StringKind, 248 arrayType: cue.ListKind, 249 objectType: cue.StructKind, 250 } 251 252 func kindToAST(k cue.Kind) ast.Expr { 253 switch k { 254 case cue.NullKind: 255 // TODO: handle OpenAPI restrictions. 256 return ast.NewNull() 257 case cue.BoolKind: 258 return ast.NewIdent("bool") 259 case cue.NumberKind: 260 return ast.NewIdent("number") 261 case cue.IntKind: 262 return ast.NewIdent("int") 263 case cue.FloatKind: 264 return ast.NewIdent("float") 265 case cue.StringKind: 266 return ast.NewIdent("string") 267 case cue.ListKind: 268 return ast.NewList(&ast.Ellipsis{}) 269 case cue.StructKind: 270 return ast.NewStruct(&ast.Ellipsis{}) 271 } 272 panic(fmt.Errorf("unexpected kind %v", k)) 273 } 274 275 var coreTypeName = []string{ 276 nullType: "null", 277 boolType: "bool", 278 numType: "number", 279 stringType: "string", 280 arrayType: "array", 281 objectType: "object", 282 } 283 284 type constraintInfo struct { 285 // typ is an identifier for the root type, if present. 286 // This can be omitted if there are constraints. 287 typ ast.Expr 288 constraints []ast.Expr 289 } 290 291 func (c *constraintInfo) setTypeUsed(n cue.Value, t coreType) { 292 c.typ = kindToAST(coreToCUE[t]) 293 setPos(c.typ, n) 294 ast.SetRelPos(c.typ, token.NoRelPos) 295 } 296 297 func (c *constraintInfo) add(n cue.Value, x ast.Expr) { 298 if !isAny(x) { 299 setPos(x, n) 300 ast.SetRelPos(x, token.NoRelPos) 301 c.constraints = append(c.constraints, x) 302 } 303 } 304 305 func (s *state) add(n cue.Value, t coreType, x ast.Expr) { 306 s.types[t].add(n, x) 307 } 308 309 func (s *state) setTypeUsed(n cue.Value, t coreType) { 310 if int(t) >= len(s.types) { 311 panic(fmt.Errorf("type out of range %v/%v", int(t), len(s.types))) 312 } 313 s.types[t].setTypeUsed(n, t) 314 } 315 316 type state struct { 317 *decoder 318 319 isSchema bool // for omitting ellipsis in an ast.File 320 321 up *state 322 parent *state 323 324 path []string 325 326 // idRef is used to refer to this schema in case it defines an $id. 327 idRef []label 328 329 pos cue.Value 330 331 // The constraints in types represent disjunctions per type. 332 types [numCoreTypes]constraintInfo 333 all constraintInfo // values and oneOf etc. 334 nullable *ast.BasicLit // nullable 335 336 // allowedTypes holds the set of types that 337 // this node is allowed to be. 338 allowedTypes cue.Kind 339 340 // knownTypes holds the set of types that this node 341 // is known to be one of by virtue of the constraints inside 342 // all. This is used to avoid adding redundant elements 343 // to the disjunction created by [state.finalize]. 344 knownTypes cue.Kind 345 346 default_ ast.Expr 347 examples []ast.Expr 348 title string 349 description string 350 deprecated bool 351 exclusiveMin bool // For OpenAPI and legacy support. 352 exclusiveMax bool // For OpenAPI and legacy support. 353 354 schemaVersion schemaVersion 355 schemaVersionPresent bool 356 357 id *url.URL // base URI for $ref 358 idPos token.Pos 359 360 definitions []ast.Decl 361 362 // Used for inserting definitions, properties, etc. 363 hasSelfReference bool 364 obj *ast.StructLit 365 // Complete at finalize. 366 fieldRefs map[label]refs 367 368 closeStruct bool 369 patterns []ast.Expr 370 371 list *ast.ListLit 372 } 373 374 type label struct { 375 name string 376 isDef bool 377 } 378 379 type refs struct { 380 field *ast.Field 381 ident string 382 refs []*ast.Ident 383 } 384 385 func (s *state) idTag() *ast.Attribute { 386 return &ast.Attribute{ 387 At: s.idPos, 388 Text: fmt.Sprintf("@jsonschema(id=%q)", s.id)} 389 } 390 391 func (s *state) object(n cue.Value) *ast.StructLit { 392 if s.obj == nil { 393 s.obj = &ast.StructLit{} 394 395 if s.id != nil { 396 s.obj.Elts = append(s.obj.Elts, s.idTag()) 397 } 398 s.add(n, objectType, s.obj) 399 } 400 return s.obj 401 } 402 403 func (s *state) hasConstraints() bool { 404 if len(s.all.constraints) > 0 { 405 return true 406 } 407 for _, t := range s.types { 408 if len(t.constraints) > 0 { 409 return true 410 } 411 } 412 return len(s.patterns) > 0 || 413 s.title != "" || 414 s.description != "" || 415 s.obj != nil || 416 s.id != nil 417 } 418 419 const allTypes = cue.NullKind | cue.BoolKind | cue.NumberKind | cue.IntKind | 420 cue.StringKind | cue.ListKind | cue.StructKind 421 422 // finalize constructs a CUE type from the collected constraints. 423 func (s *state) finalize() (e ast.Expr) { 424 if s.allowedTypes == 0 { 425 // Nothing is possible. 426 s.addErr(errors.Newf(s.pos.Pos(), "constraints are not possible to satisfy")) 427 return &ast.BottomLit{} 428 } 429 430 conjuncts := []ast.Expr{} 431 disjuncts := []ast.Expr{} 432 433 // Sort literal structs and list last for nicer formatting. 434 sort.SliceStable(s.types[arrayType].constraints, func(i, j int) bool { 435 _, ok := s.types[arrayType].constraints[i].(*ast.ListLit) 436 return !ok 437 }) 438 sort.SliceStable(s.types[objectType].constraints, func(i, j int) bool { 439 _, ok := s.types[objectType].constraints[i].(*ast.StructLit) 440 return !ok 441 }) 442 443 type excludeInfo struct { 444 pos token.Pos 445 typIndex int 446 } 447 var excluded []excludeInfo 448 449 needsTypeDisjunction := s.allowedTypes != s.knownTypes 450 if !needsTypeDisjunction { 451 for i, t := range s.types { 452 k := coreToCUE[i] 453 if len(t.constraints) > 0 && s.allowedTypes&k != 0 { 454 // We need to include at least one type-specific 455 // constraint in the disjunction. 456 needsTypeDisjunction = true 457 break 458 } 459 } 460 } 461 462 if needsTypeDisjunction { 463 npossible := 0 464 nexcluded := 0 465 for i, t := range s.types { 466 k := coreToCUE[i] 467 allowed := s.allowedTypes&k != 0 468 switch { 469 case len(t.constraints) > 0: 470 npossible++ 471 if !allowed { 472 nexcluded++ 473 for _, c := range t.constraints { 474 excluded = append(excluded, excludeInfo{c.Pos(), i}) 475 } 476 continue 477 } 478 x := ast.NewBinExpr(token.AND, t.constraints...) 479 disjuncts = append(disjuncts, x) 480 case allowed: 481 npossible++ 482 if s.knownTypes&k != 0 { 483 disjuncts = append(disjuncts, kindToAST(k)) 484 } 485 } 486 } 487 if nexcluded == npossible { 488 // All possibilities have been excluded: this is an impossible 489 // schema. 490 for _, e := range excluded { 491 s.addErr(errors.Newf(e.pos, 492 "constraint not allowed because type %s is excluded", 493 coreTypeName[e.typIndex], 494 )) 495 } 496 } 497 } 498 conjuncts = append(conjuncts, s.all.constraints...) 499 obj := s.obj 500 if obj == nil { 501 obj, _ = s.types[objectType].typ.(*ast.StructLit) 502 } 503 if obj != nil { 504 // TODO: may need to explicitly close. 505 if !s.closeStruct { 506 obj.Elts = append(obj.Elts, &ast.Ellipsis{}) 507 } 508 } 509 510 if len(disjuncts) > 0 { 511 conjuncts = append(conjuncts, ast.NewBinExpr(token.OR, disjuncts...)) 512 } 513 514 if len(conjuncts) == 0 { 515 // There are no conjuncts, which can only happen when there 516 // are no disjuncts, which can only happen when the entire 517 // set of disjuncts is redundant with respect to the types 518 // already implied by s.all. As we've already checked that 519 // s.allowedTypes is non-zero (so we know that 520 // it's not bottom) and we need _some_ expression 521 // to be part of the subequent syntax, we use top. 522 e = ast.NewIdent("_") 523 } else { 524 e = ast.NewBinExpr(token.AND, conjuncts...) 525 } 526 527 a := []ast.Expr{e} 528 if s.nullable != nil { 529 a = []ast.Expr{s.nullable, e} 530 } 531 532 outer: 533 switch { 534 case s.default_ != nil: 535 // check conditions where default can be skipped. 536 switch x := s.default_.(type) { 537 case *ast.ListLit: 538 if s.allowedTypes == cue.ListKind && len(x.Elts) == 0 { 539 break outer 540 } 541 } 542 a = append(a, &ast.UnaryExpr{Op: token.MUL, X: s.default_}) 543 } 544 545 e = ast.NewBinExpr(token.OR, a...) 546 547 if len(s.definitions) > 0 { 548 if st, ok := e.(*ast.StructLit); ok { 549 st.Elts = append(st.Elts, s.definitions...) 550 } else { 551 st = ast.NewStruct() 552 st.Elts = append(st.Elts, &ast.EmbedDecl{Expr: e}) 553 st.Elts = append(st.Elts, s.definitions...) 554 e = st 555 } 556 } 557 558 s.linkReferences() 559 560 // If an "$id" exists and has not been included in any object constraints 561 if s.id != nil && s.obj == nil { 562 if st, ok := e.(*ast.StructLit); ok { 563 st.Elts = append([]ast.Decl{s.idTag()}, st.Elts...) 564 } else { 565 st = &ast.StructLit{Elts: []ast.Decl{s.idTag()}} 566 st.Elts = append(st.Elts, &ast.EmbedDecl{Expr: e}) 567 e = st 568 } 569 } 570 571 // Now that we've expressed the schema as actual syntax, 572 // all the allowed types are actually explicit and will not 573 // need to be mentioned again. 574 s.knownTypes = s.allowedTypes 575 return e 576 } 577 578 func isAny(s ast.Expr) bool { 579 i, ok := s.(*ast.Ident) 580 return ok && i.Name == "_" 581 } 582 583 func (s *state) comment() *ast.CommentGroup { 584 // Create documentation. 585 doc := strings.TrimSpace(s.title) 586 if s.description != "" { 587 if doc != "" { 588 doc += "\n\n" 589 } 590 doc += s.description 591 doc = strings.TrimSpace(doc) 592 } 593 // TODO: add examples as well? 594 if doc == "" { 595 return nil 596 } 597 return internal.NewComment(true, doc) 598 } 599 600 func (s *state) doc(n ast.Node) { 601 doc := s.comment() 602 if doc != nil { 603 ast.SetComments(n, []*ast.CommentGroup{doc}) 604 } 605 } 606 607 func (s *state) schema(n cue.Value, idRef ...label) ast.Expr { 608 expr, _ := s.schemaState(n, allTypes, idRef, false) 609 // TODO: report unused doc. 610 return expr 611 } 612 613 // schemaState returns a new state value derived from s. 614 // n holds the JSONSchema node to translate to a schema. 615 // types holds the set of possible types that the value can hold. 616 // idRef holds the path to the value. 617 // isLogical specifies whether the caller is a logical operator like anyOf, allOf, oneOf, or not. 618 func (s *state) schemaState(n cue.Value, types cue.Kind, idRef []label, isLogical bool) (_e ast.Expr, _ *state) { 619 state := &state{ 620 up: s, 621 schemaVersion: s.schemaVersion, 622 isSchema: s.isSchema, 623 decoder: s.decoder, 624 allowedTypes: types, 625 knownTypes: allTypes, 626 path: s.path, 627 idRef: idRef, 628 pos: n, 629 } 630 if isLogical { 631 state.parent = s 632 } 633 634 if n.Kind() != cue.StructKind { 635 return s.errf(n, "schema expects mapping node, found %s", n.Kind()), state 636 } 637 638 // do multiple passes over the constraints to ensure they are done in order. 639 for pass := 0; pass < 4; pass++ { 640 state.processMap(n, func(key string, value cue.Value) { 641 // Convert each constraint into a either a value or a functor. 642 c := constraintMap[key] 643 if c == nil { 644 if pass == 0 && s.cfg.Strict { 645 // TODO: value is not the correct position, albeit close. Fix this. 646 s.warnf(value.Pos(), "unsupported constraint %q", key) 647 } 648 return 649 } 650 if c.phase == pass { 651 c.fn(key, value, state) 652 } 653 }) 654 } 655 656 return state.finalize(), state 657 } 658 659 func (s *state) value(n cue.Value) ast.Expr { 660 k := n.Kind() 661 switch k { 662 case cue.ListKind: 663 a := []ast.Expr{} 664 for i, _ := n.List(); i.Next(); { 665 a = append(a, s.value(i.Value())) 666 } 667 return setPos(ast.NewList(a...), n) 668 669 case cue.StructKind: 670 a := []ast.Decl{} 671 s.processMap(n, func(key string, n cue.Value) { 672 a = append(a, &ast.Field{ 673 Label: ast.NewString(key), 674 Value: s.value(n), 675 }) 676 }) 677 // TODO: only open when s.isSchema? 678 a = append(a, &ast.Ellipsis{}) 679 return setPos(&ast.StructLit{Elts: a}, n) 680 681 default: 682 if !n.IsConcrete() { 683 s.errf(n, "invalid non-concrete value") 684 } 685 return n.Syntax(cue.Final()).(ast.Expr) 686 } 687 } 688 689 // processMap processes a yaml node, expanding merges. 690 // 691 // TODO: in some cases we can translate merges into CUE embeddings. 692 // This may also prevent exponential blow-up (as may happen when 693 // converting YAML to JSON). 694 func (s *state) processMap(n cue.Value, f func(key string, n cue.Value)) { 695 saved := s.path 696 defer func() { s.path = saved }() 697 698 // TODO: intercept references to allow for optimized performance. 699 for i, _ := n.Fields(); i.Next(); { 700 key := i.Label() 701 s.path = append(saved, key) 702 f(key, i.Value()) 703 } 704 } 705 706 func (s *state) listItems(name string, n cue.Value, allowEmpty bool) (a []cue.Value) { 707 if n.Kind() != cue.ListKind { 708 s.errf(n, `value of %q must be an array, found %v`, name, n.Kind()) 709 } 710 for i, _ := n.List(); i.Next(); { 711 a = append(a, i.Value()) 712 } 713 if !allowEmpty && len(a) == 0 { 714 s.errf(n, `array for %q must be non-empty`, name) 715 } 716 return a 717 } 718 719 // excludeFields returns a CUE expression that can be used to exclude the 720 // fields of the given declaration in a label expression. For instance, for 721 // 722 // { foo: 1, bar: int } 723 // 724 // it creates 725 // 726 // "^(foo|bar)$" 727 // 728 // which can be used in a label expression to define types for all fields but 729 // those existing: 730 // 731 // [!~"^(foo|bar)$"]: string 732 func excludeFields(decls []ast.Decl) ast.Expr { 733 var a []string 734 for _, d := range decls { 735 f, ok := d.(*ast.Field) 736 if !ok { 737 continue 738 } 739 str, _, _ := ast.LabelName(f.Label) 740 if str != "" { 741 a = append(a, str) 742 } 743 } 744 re := fmt.Sprintf("^(%s)$", strings.Join(a, "|")) 745 return &ast.UnaryExpr{Op: token.NMAT, X: ast.NewString(re)} 746 } 747 748 func addTag(field ast.Label, tag, value string) *ast.Field { 749 return &ast.Field{ 750 Label: field, 751 Value: ast.NewIdent("_"), 752 Attrs: []*ast.Attribute{ 753 {Text: fmt.Sprintf("@%s(%s)", tag, value)}, 754 }, 755 } 756 } 757 758 func setPos(e ast.Expr, v cue.Value) ast.Expr { 759 ast.SetPos(e, v.Pos()) 760 return e 761 }