github.com/solo-io/cue@v0.4.7/encoding/jsonschema/constraints.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 import ( 18 "fmt" 19 "math/big" 20 "path" 21 "regexp" 22 23 "github.com/solo-io/cue/cue" 24 "github.com/solo-io/cue/cue/ast" 25 "github.com/solo-io/cue/cue/errors" 26 "github.com/solo-io/cue/cue/token" 27 "github.com/solo-io/cue/internal" 28 ) 29 30 // TODO: skip invalid regexps containing ?! and foes. 31 // alternatively, fall back to https://github.com/dlclark/regexp2 32 33 type constraint struct { 34 key string 35 36 // phase indicates on which pass c constraint should be added. This ensures 37 // that constraints are applied in the correct order. For instance, the 38 // "required" constraint validates that a listed field is contained in 39 // "properties". For this to work, "properties" must be processed before 40 // "required" and thus must have a lower phase number than the latter. 41 phase int 42 43 // Indicates the draft number in which this constraint is defined. 44 draft int 45 fn constraintFunc 46 } 47 48 // A constraintFunc converts a given JSON Schema constraint (specified in n) 49 // to a CUE constraint recorded in state. 50 type constraintFunc func(n cue.Value, s *state) 51 52 func p0(name string, f constraintFunc) *constraint { 53 return &constraint{key: name, fn: f} 54 } 55 56 func p1d(name string, draft int, f constraintFunc) *constraint { 57 return &constraint{key: name, phase: 1, draft: draft, fn: f} 58 } 59 60 func p1(name string, f constraintFunc) *constraint { 61 return &constraint{key: name, phase: 1, fn: f} 62 } 63 64 func p2(name string, f constraintFunc) *constraint { 65 return &constraint{key: name, phase: 2, fn: f} 66 } 67 68 func p3(name string, f constraintFunc) *constraint { 69 return &constraint{key: name, phase: 3, fn: f} 70 } 71 72 // TODO: 73 // writeOnly, readOnly 74 75 var constraintMap = map[string]*constraint{} 76 77 func init() { 78 for _, c := range constraints { 79 constraintMap[c.key] = c 80 } 81 } 82 83 func addDefinitions(n cue.Value, s *state) { 84 if n.Kind() != cue.StructKind { 85 s.errf(n, `"definitions" expected an object, found %s`, n.Kind()) 86 } 87 88 old := s.isSchema 89 s.isSchema = true 90 defer func() { s.isSchema = old }() 91 92 s.processMap(n, func(key string, n cue.Value) { 93 name := key 94 95 var f *ast.Field 96 97 ident := "#" + name 98 if ast.IsValidIdent(ident) { 99 f = &ast.Field{Value: s.schema(n, label{ident, true})} 100 f.Label = ast.NewIdent(ident) 101 } else { 102 f = &ast.Field{Value: s.schema(n, label{"#", true}, label{name: name})} 103 f.Label = ast.NewString(name) 104 ident = "#" 105 f = &ast.Field{ 106 Label: ast.NewIdent("#"), 107 Value: ast.NewStruct(f), 108 } 109 } 110 111 ast.SetRelPos(f, token.NewSection) 112 s.definitions = append(s.definitions, f) 113 s.setField(label{name: ident, isDef: true}, f) 114 }) 115 } 116 117 var constraints = []*constraint{ 118 // Meta data. 119 120 p0("$schema", func(n cue.Value, s *state) { 121 // Identifies this as a JSON schema and specifies its version. 122 // TODO: extract version. 123 s.jsonschema, _ = s.strValue(n) 124 }), 125 126 p0("$id", func(n cue.Value, s *state) { 127 // URL: https://domain.com/schemas/foo.json 128 // anchors: #identifier 129 // 130 // TODO: mark identifiers. 131 132 // Resolution must be relative to parent $id 133 // https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2 134 u := s.resolveURI(n) 135 if u == nil { 136 return 137 } 138 139 if u.Fragment != "" { 140 if s.cfg.Strict { 141 s.errf(n, "$id URI may not contain a fragment") 142 } 143 return 144 } 145 s.id = u 146 147 obj := s.object(n) 148 149 // TODO: handle the case where this is always defined and we don't want 150 // to include the default value. 151 obj.Elts = append(obj.Elts, &ast.Attribute{ 152 Text: fmt.Sprintf("@jsonschema(id=%q)", u)}) 153 }), 154 155 // Generic constraint 156 157 p1("type", func(n cue.Value, s *state) { 158 var types cue.Kind 159 set := func(n cue.Value) { 160 str, ok := s.strValue(n) 161 if !ok { 162 s.errf(n, "type value should be a string") 163 } 164 switch str { 165 case "null": 166 types |= cue.NullKind 167 s.setTypeUsed(n, nullType) 168 // TODO: handle OpenAPI restrictions. 169 case "boolean": 170 types |= cue.BoolKind 171 s.setTypeUsed(n, boolType) 172 case "string": 173 types |= cue.StringKind 174 s.setTypeUsed(n, stringType) 175 case "number": 176 types |= cue.NumberKind 177 s.setTypeUsed(n, numType) 178 case "integer": 179 types |= cue.IntKind 180 s.setTypeUsed(n, numType) 181 s.add(n, numType, ast.NewIdent("int")) 182 case "array": 183 types |= cue.ListKind 184 s.setTypeUsed(n, arrayType) 185 case "object": 186 types |= cue.StructKind 187 s.setTypeUsed(n, objectType) 188 189 default: 190 s.errf(n, "unknown type %q", n) 191 } 192 } 193 194 switch n.Kind() { 195 case cue.StringKind: 196 set(n) 197 case cue.ListKind: 198 for i, _ := n.List(); i.Next(); { 199 set(i.Value()) 200 } 201 default: 202 s.errf(n, `value of "type" must be a string or list of strings`) 203 } 204 205 s.allowedTypes &= types 206 }), 207 208 p1("enum", func(n cue.Value, s *state) { 209 var a []ast.Expr 210 for _, x := range s.listItems("enum", n, true) { 211 a = append(a, s.value(x)) 212 } 213 s.all.add(n, ast.NewBinExpr(token.OR, a...)) 214 }), 215 216 // TODO: only allow for OpenAPI. 217 p1("nullable", func(n cue.Value, s *state) { 218 null := ast.NewNull() 219 setPos(null, n) 220 s.nullable = null 221 }), 222 223 p1d("const", 6, func(n cue.Value, s *state) { 224 s.all.add(n, s.value(n)) 225 }), 226 227 p1("default", func(n cue.Value, s *state) { 228 sc := *s 229 s.default_ = sc.value(n) 230 // TODO: must validate that the default is subsumed by the normal value, 231 // as CUE will otherwise broaden the accepted values with the default. 232 s.examples = append(s.examples, s.default_) 233 }), 234 235 p1("deprecated", func(n cue.Value, s *state) { 236 if s.boolValue(n) { 237 s.deprecated = true 238 } 239 }), 240 241 p1("examples", func(n cue.Value, s *state) { 242 if n.Kind() != cue.ListKind { 243 s.errf(n, `value of "examples" must be an array, found %v`, n.Kind) 244 } 245 // TODO: implement examples properly. 246 // for _, n := range s.listItems("examples", n, true) { 247 // if ex := s.value(n); !isAny(ex) { 248 // s.examples = append(s.examples, ex) 249 // } 250 // } 251 }), 252 253 p1("description", func(n cue.Value, s *state) { 254 s.description, _ = s.strValue(n) 255 }), 256 257 p1("title", func(n cue.Value, s *state) { 258 s.title, _ = s.strValue(n) 259 }), 260 261 p1d("$comment", 7, func(n cue.Value, s *state) { 262 }), 263 264 p1("$defs", addDefinitions), 265 p1("definitions", addDefinitions), 266 p1("$ref", func(n cue.Value, s *state) { 267 s.usedTypes = allTypes 268 269 u := s.resolveURI(n) 270 271 if u.Fragment != "" && !path.IsAbs(u.Fragment) { 272 s.addErr(errors.Newf(n.Pos(), "anchors (%s) not supported", u.Fragment)) 273 // TODO: support anchors 274 return 275 } 276 277 expr := s.makeCUERef(n, u) 278 279 if expr == nil { 280 expr = &ast.BadExpr{From: n.Pos()} 281 } 282 283 s.all.add(n, expr) 284 }), 285 286 // Combinators 287 288 // TODO: work this out in more detail: oneOf and anyOf below have the same 289 // implementation in CUE. The distinction is that for anyOf a result is 290 // allowed to be ambiguous at the end, whereas for oneOf a disjunction must 291 // be fully resolved. There is currently no easy way to set this distinction 292 // in CUE. 293 // 294 // One could correctly write oneOf like this once 'not' is implemented: 295 // 296 // oneOf(a, b, c) :- 297 // anyOf( 298 // allOf(a, not(b), not(c)), 299 // allOf(not(a), b, not(c)), 300 // allOf(not(a), not(b), c), 301 // )) 302 // 303 // This is not necessary if the values are mutually exclusive/ have a 304 // discriminator. 305 306 p2("allOf", func(n cue.Value, s *state) { 307 var a []ast.Expr 308 for _, v := range s.listItems("allOf", n, false) { 309 x, sub := s.schemaState(v, s.allowedTypes, nil, true) 310 s.allowedTypes &= sub.allowedTypes 311 s.usedTypes |= sub.usedTypes 312 if sub.hasConstraints() { 313 a = append(a, x) 314 } 315 } 316 if len(a) > 0 { 317 s.all.add(n, ast.NewBinExpr(token.AND, a...)) 318 } 319 }), 320 321 p2("anyOf", func(n cue.Value, s *state) { 322 var types cue.Kind 323 var a []ast.Expr 324 for _, v := range s.listItems("anyOf", n, false) { 325 x, sub := s.schemaState(v, s.allowedTypes, nil, true) 326 types |= sub.allowedTypes 327 a = append(a, x) 328 } 329 s.allowedTypes &= types 330 if len(a) > 0 { 331 s.all.add(n, ast.NewBinExpr(token.OR, a...)) 332 } 333 }), 334 335 p2("oneOf", func(n cue.Value, s *state) { 336 var types cue.Kind 337 var a []ast.Expr 338 hasSome := false 339 for _, v := range s.listItems("oneOf", n, false) { 340 x, sub := s.schemaState(v, s.allowedTypes, nil, true) 341 types |= sub.allowedTypes 342 343 // TODO: make more finegrained by making it two pass. 344 if sub.hasConstraints() { 345 hasSome = true 346 } 347 348 if !isAny(x) { 349 a = append(a, x) 350 } 351 } 352 s.allowedTypes &= types 353 if len(a) > 0 && hasSome { 354 s.usedTypes = allTypes 355 s.all.add(n, ast.NewBinExpr(token.OR, a...)) 356 } 357 358 // TODO: oneOf({a:x}, {b:y}, ..., not(anyOf({a:x}, {b:y}, ...))), 359 // can be translated to {} | {a:x}, {b:y}, ... 360 }), 361 362 // String constraints 363 364 p1("pattern", func(n cue.Value, s *state) { 365 str, _ := n.String() 366 if _, err := regexp.Compile(str); err != nil { 367 if s.cfg.Strict { 368 s.errf(n, "unsupported regexp: %v", err) 369 } 370 return 371 } 372 s.usedTypes |= cue.StringKind 373 s.add(n, stringType, &ast.UnaryExpr{Op: token.MAT, X: s.string(n)}) 374 }), 375 376 p1("minLength", func(n cue.Value, s *state) { 377 s.usedTypes |= cue.StringKind 378 min := s.number(n) 379 strings := s.addImport(n, "strings") 380 s.add(n, stringType, ast.NewCall(ast.NewSel(strings, "MinRunes"), min)) 381 }), 382 383 p1("maxLength", func(n cue.Value, s *state) { 384 s.usedTypes |= cue.StringKind 385 max := s.number(n) 386 strings := s.addImport(n, "strings") 387 s.add(n, stringType, ast.NewCall(ast.NewSel(strings, "MaxRunes"), max)) 388 }), 389 390 p1d("contentMediaType", 7, func(n cue.Value, s *state) { 391 // TODO: only mark as used if it generates something. 392 // s.usedTypes |= cue.StringKind 393 }), 394 395 p1d("contentEncoding", 7, func(n cue.Value, s *state) { 396 // TODO: only mark as used if it generates something. 397 // s.usedTypes |= cue.StringKind 398 // 7bit, 8bit, binary, quoted-printable and base64. 399 // RFC 2054, part 6.1. 400 // https://tools.ietf.org/html/rfc2045 401 // TODO: at least handle bytes. 402 }), 403 404 // Number constraints 405 406 p2("minimum", func(n cue.Value, s *state) { 407 s.usedTypes |= cue.NumberKind 408 op := token.GEQ 409 if s.exclusiveMin { 410 op = token.GTR 411 } 412 s.add(n, numType, &ast.UnaryExpr{Op: op, X: s.number(n)}) 413 }), 414 415 p1("exclusiveMinimum", func(n cue.Value, s *state) { 416 if n.Kind() == cue.BoolKind { 417 s.exclusiveMin = true 418 return 419 } 420 s.usedTypes |= cue.NumberKind 421 s.add(n, numType, &ast.UnaryExpr{Op: token.GTR, X: s.number(n)}) 422 }), 423 424 p2("maximum", func(n cue.Value, s *state) { 425 s.usedTypes |= cue.NumberKind 426 op := token.LEQ 427 if s.exclusiveMax { 428 op = token.LSS 429 } 430 s.add(n, numType, &ast.UnaryExpr{Op: op, X: s.number(n)}) 431 }), 432 433 p1("exclusiveMaximum", func(n cue.Value, s *state) { 434 if n.Kind() == cue.BoolKind { 435 s.exclusiveMax = true 436 return 437 } 438 s.usedTypes |= cue.NumberKind 439 s.add(n, numType, &ast.UnaryExpr{Op: token.LSS, X: s.number(n)}) 440 }), 441 442 p1("multipleOf", func(n cue.Value, s *state) { 443 s.usedTypes |= cue.NumberKind 444 multiple := s.number(n) 445 var x big.Int 446 _, _ = n.MantExp(&x) 447 if x.Cmp(big.NewInt(0)) != 1 { 448 s.errf(n, `"multipleOf" value must be < 0; found %s`, n) 449 } 450 math := s.addImport(n, "math") 451 s.add(n, numType, ast.NewCall(ast.NewSel(math, "MultipleOf"), multiple)) 452 }), 453 454 // Object constraints 455 456 p1("properties", func(n cue.Value, s *state) { 457 s.usedTypes |= cue.StructKind 458 obj := s.object(n) 459 460 if n.Kind() != cue.StructKind { 461 s.errf(n, `"properties" expected an object, found %v`, n.Kind()) 462 } 463 464 s.processMap(n, func(key string, n cue.Value) { 465 // property?: value 466 name := ast.NewString(key) 467 expr, state := s.schemaState(n, allTypes, []label{{name: key}}, false) 468 f := &ast.Field{Label: name, Value: expr} 469 state.doc(f) 470 f.Optional = token.Blank.Pos() 471 if len(obj.Elts) > 0 && len(f.Comments()) > 0 { 472 // TODO: change formatter such that either a a NewSection on the 473 // field or doc comment will cause a new section. 474 ast.SetRelPos(f.Comments()[0], token.NewSection) 475 } 476 if state.deprecated { 477 switch expr.(type) { 478 case *ast.StructLit: 479 obj.Elts = append(obj.Elts, addTag(name, "deprecated", "")) 480 default: 481 f.Attrs = append(f.Attrs, internal.NewAttr("deprecated", "")) 482 } 483 } 484 obj.Elts = append(obj.Elts, f) 485 s.setField(label{name: key}, f) 486 }) 487 }), 488 489 p2("required", func(n cue.Value, s *state) { 490 if n.Kind() != cue.ListKind { 491 s.errf(n, `value of "required" must be list of strings, found %v`, n.Kind) 492 return 493 } 494 495 s.usedTypes |= cue.StructKind 496 497 // TODO: detect that properties is defined somewhere. 498 // s.errf(n, `"required" without a "properties" field`) 499 obj := s.object(n) 500 501 // Create field map 502 fields := map[string]*ast.Field{} 503 for _, d := range obj.Elts { 504 f, ok := d.(*ast.Field) 505 if !ok { 506 continue // Could be embedding? See cirrus.json 507 } 508 str, _, err := ast.LabelName(f.Label) 509 if err == nil { 510 fields[str] = f 511 } 512 } 513 514 for _, n := range s.listItems("required", n, true) { 515 str, ok := s.strValue(n) 516 f := fields[str] 517 if f == nil && ok { 518 f := &ast.Field{ 519 Label: ast.NewString(str), 520 Value: ast.NewIdent("_"), 521 } 522 fields[str] = f 523 obj.Elts = append(obj.Elts, f) 524 continue 525 } 526 if f.Optional == token.NoPos { 527 s.errf(n, "duplicate required field %q", str) 528 } 529 f.Optional = token.NoPos 530 } 531 }), 532 533 p1d("propertyNames", 6, func(n cue.Value, s *state) { 534 // [=~pattern]: _ 535 if names, _ := s.schemaState(n, cue.StringKind, nil, false); !isAny(names) { 536 s.usedTypes |= cue.StructKind 537 x := ast.NewStruct(ast.NewList(names), ast.NewIdent("_")) 538 s.add(n, objectType, x) 539 } 540 }), 541 542 // TODO: reenable when we have proper non-monotonic contraint validation. 543 // p1("minProperties", func(n cue.Value, s *state) { 544 // s.usedTypes |= cue.StructKind 545 546 // pkg := s.addImport(n, "struct") 547 // s.addConjunct(n, ast.NewCall(ast.NewSel(pkg, "MinFields"), s.uint(n))) 548 // }), 549 550 p1("maxProperties", func(n cue.Value, s *state) { 551 s.usedTypes |= cue.StructKind 552 553 pkg := s.addImport(n, "struct") 554 x := ast.NewCall(ast.NewSel(pkg, "MaxFields"), s.uint(n)) 555 s.add(n, objectType, x) 556 }), 557 558 p1("dependencies", func(n cue.Value, s *state) { 559 s.usedTypes |= cue.StructKind 560 561 // Schema and property dependencies. 562 // TODO: the easiest implementation is with comprehensions. 563 // The nicer implementation is with disjunctions. This has to be done 564 // at the very end, replacing properties. 565 /* 566 *{ property?: _|_ } | { 567 property: _ 568 schema 569 } 570 */ 571 }), 572 573 p2("patternProperties", func(n cue.Value, s *state) { 574 s.usedTypes |= cue.StructKind 575 if n.Kind() != cue.StructKind { 576 s.errf(n, `value of "patternProperties" must be an an object, found %v`, n.Kind) 577 } 578 obj := s.object(n) 579 existing := excludeFields(s.obj.Elts) 580 s.processMap(n, func(key string, n cue.Value) { 581 // [!~(properties) & pattern]: schema 582 s.patterns = append(s.patterns, 583 &ast.UnaryExpr{Op: token.NMAT, X: ast.NewString(key)}) 584 f := internal.EmbedStruct(ast.NewStruct(&ast.Field{ 585 Label: ast.NewList(ast.NewBinExpr(token.AND, 586 &ast.UnaryExpr{Op: token.MAT, X: ast.NewString(key)}, 587 existing)), 588 Value: s.schema(n), 589 })) 590 ast.SetRelPos(f, token.NewSection) 591 obj.Elts = append(obj.Elts, f) 592 }) 593 }), 594 595 p3("additionalProperties", func(n cue.Value, s *state) { 596 switch n.Kind() { 597 case cue.BoolKind: 598 s.closeStruct = !s.boolValue(n) 599 600 case cue.StructKind: 601 s.usedTypes |= cue.StructKind 602 s.closeStruct = true 603 obj := s.object(n) 604 if len(obj.Elts) == 0 { 605 obj.Elts = append(obj.Elts, &ast.Field{ 606 Label: ast.NewList(ast.NewIdent("string")), 607 Value: s.schema(n), 608 }) 609 return 610 } 611 // [!~(properties|patternProperties)]: schema 612 existing := append(s.patterns, excludeFields(obj.Elts)) 613 f := internal.EmbedStruct(ast.NewStruct(&ast.Field{ 614 Label: ast.NewList(ast.NewBinExpr(token.AND, existing...)), 615 Value: s.schema(n), 616 })) 617 obj.Elts = append(obj.Elts, f) 618 619 default: 620 s.errf(n, `value of "additionalProperties" must be an object or boolean`) 621 } 622 }), 623 624 // Array constraints. 625 626 p1("items", func(n cue.Value, s *state) { 627 s.usedTypes |= cue.ListKind 628 switch n.Kind() { 629 case cue.StructKind: 630 elem := s.schema(n) 631 ast.SetRelPos(elem, token.NoRelPos) 632 s.add(n, arrayType, ast.NewList(&ast.Ellipsis{Type: elem})) 633 634 case cue.ListKind: 635 var a []ast.Expr 636 for _, n := range s.listItems("items", n, true) { 637 v := s.schema(n) // TODO: label with number literal. 638 ast.SetRelPos(v, token.NoRelPos) 639 a = append(a, v) 640 } 641 s.list = ast.NewList(a...) 642 s.add(n, arrayType, s.list) 643 644 default: 645 s.errf(n, `value of "items" must be an object or array`) 646 } 647 }), 648 649 p1("additionalItems", func(n cue.Value, s *state) { 650 switch n.Kind() { 651 case cue.BoolKind: 652 // TODO: support 653 654 case cue.StructKind: 655 if s.list != nil { 656 s.usedTypes |= cue.ListKind 657 elem := s.schema(n) 658 s.list.Elts = append(s.list.Elts, &ast.Ellipsis{Type: elem}) 659 } 660 661 default: 662 s.errf(n, `value of "additionalItems" must be an object or boolean`) 663 } 664 }), 665 666 p1("contains", func(n cue.Value, s *state) { 667 s.usedTypes |= cue.ListKind 668 list := s.addImport(n, "list") 669 // TODO: Passing non-concrete values is not yet supported in CUE. 670 if x := s.schema(n); !isAny(x) { 671 x := ast.NewCall(ast.NewSel(list, "Contains"), clearPos(x)) 672 s.add(n, arrayType, x) 673 } 674 }), 675 676 // TODO: min/maxContains 677 678 p1("minItems", func(n cue.Value, s *state) { 679 s.usedTypes |= cue.ListKind 680 a := []ast.Expr{} 681 p, err := n.Uint64() 682 if err != nil { 683 s.errf(n, "invalid uint") 684 } 685 for ; p > 0; p-- { 686 a = append(a, ast.NewIdent("_")) 687 } 688 s.add(n, arrayType, ast.NewList(append(a, &ast.Ellipsis{})...)) 689 690 // TODO: use this once constraint resolution is properly implemented. 691 // list := s.addImport(n, "list") 692 // s.addConjunct(n, ast.NewCall(ast.NewSel(list, "MinItems"), clearPos(s.uint(n)))) 693 }), 694 695 p1("maxItems", func(n cue.Value, s *state) { 696 s.usedTypes |= cue.ListKind 697 list := s.addImport(n, "list") 698 x := ast.NewCall(ast.NewSel(list, "MaxItems"), clearPos(s.uint(n))) 699 s.add(n, arrayType, x) 700 701 }), 702 703 p1("uniqueItems", func(n cue.Value, s *state) { 704 s.usedTypes |= cue.ListKind 705 if s.boolValue(n) { 706 list := s.addImport(n, "list") 707 s.add(n, arrayType, ast.NewCall(ast.NewSel(list, "UniqueItems"))) 708 } 709 }), 710 } 711 712 func clearPos(e ast.Expr) ast.Expr { 713 ast.SetRelPos(e, token.NoRelPos) 714 return e 715 }