github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/encoding/openapi/build.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 openapi 16 17 import ( 18 "fmt" 19 "math" 20 "path" 21 "regexp" 22 "sort" 23 "strconv" 24 "strings" 25 26 "github.com/joomcode/cue/cue" 27 "github.com/joomcode/cue/cue/ast" 28 "github.com/joomcode/cue/cue/errors" 29 "github.com/joomcode/cue/cue/token" 30 "github.com/joomcode/cue/internal" 31 "github.com/joomcode/cue/internal/core/adt" 32 ) 33 34 type buildContext struct { 35 inst *cue.Instance 36 instExt *cue.Instance 37 refPrefix string 38 path []string 39 errs errors.Error 40 41 expandRefs bool 42 structural bool 43 exclusiveBool bool 44 nameFunc func(inst *cue.Instance, path []string) string 45 descFunc func(v cue.Value) string 46 fieldFilter *regexp.Regexp 47 evalDepth int // detect cycles when resolving references 48 49 schemas *OrderedMap 50 51 // Track external schemas. 52 externalRefs map[string]*externalType 53 54 // Used for cycle detection in case of using ExpandReferences. At the 55 // moment, CUE does not detect cycles when a user forcefully steps into a 56 // pattern constraint. 57 // 58 // TODO: consider an option in the CUE API where optional fields are 59 // recursively evaluated. 60 cycleNodes []*adt.Vertex 61 } 62 63 type externalType struct { 64 ref string 65 inst *cue.Instance 66 path []string 67 value cue.Value 68 } 69 70 type oaSchema = OrderedMap 71 72 type typeFunc func(b *builder, a cue.Value) 73 74 func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err error) { 75 var fieldFilter *regexp.Regexp 76 if g.FieldFilter != "" { 77 fieldFilter, err = regexp.Compile(g.FieldFilter) 78 if err != nil { 79 return nil, errors.Newf(token.NoPos, "invalid field filter: %v", err) 80 } 81 82 // verify that certain elements are still passed. 83 for _, f := range strings.Split( 84 "version,title,allOf,anyOf,not,enum,Schema/properties,Schema/items"+ 85 "nullable,type", ",") { 86 if fieldFilter.MatchString(f) { 87 return nil, errors.Newf(token.NoPos, "field filter may not exclude %q", f) 88 } 89 } 90 } 91 92 if g.Version == "" { 93 g.Version = "3.0.0" 94 } 95 96 c := buildContext{ 97 inst: inst, 98 instExt: inst, 99 refPrefix: "components/schemas", 100 expandRefs: g.ExpandReferences, 101 structural: g.ExpandReferences, 102 nameFunc: g.ReferenceFunc, 103 descFunc: g.DescriptionFunc, 104 schemas: &OrderedMap{}, 105 externalRefs: map[string]*externalType{}, 106 fieldFilter: fieldFilter, 107 } 108 109 switch g.Version { 110 case "3.0.0": 111 c.exclusiveBool = true 112 case "3.1.0": 113 default: 114 return nil, errors.Newf(token.NoPos, "unsupported version %s", g.Version) 115 } 116 117 defer func() { 118 switch x := recover().(type) { 119 case nil: 120 case *openapiError: 121 err = x 122 default: 123 panic(x) 124 } 125 }() 126 127 // Although paths is empty for now, it makes it valid OpenAPI spec. 128 129 i, err := inst.Value().Fields(cue.Definitions(true)) 130 if err != nil { 131 return nil, err 132 } 133 for i.Next() { 134 if !i.IsDefinition() { 135 continue 136 } 137 // message, enum, or constant. 138 label := i.Label() 139 if c.isInternal(label) { 140 continue 141 } 142 if i.IsDefinition() && strings.HasPrefix(label, "#") { 143 label = label[1:] 144 } 145 ref := c.makeRef(inst, []string{label}) 146 if ref == "" { 147 continue 148 } 149 c.schemas.Set(ref, c.build(label, i.Value())) 150 } 151 152 // keep looping until a fixed point is reached. 153 for done := 0; len(c.externalRefs) != done; { 154 done = len(c.externalRefs) 155 156 // From now on, all references need to be expanded 157 external := []string{} 158 for k := range c.externalRefs { 159 external = append(external, k) 160 } 161 sort.Strings(external) 162 163 for _, k := range external { 164 ext := c.externalRefs[k] 165 c.instExt = ext.inst 166 last := len(ext.path) - 1 167 c.path = ext.path[:last] 168 name := ext.path[last] 169 c.schemas.Set(ext.ref, c.build(name, cue.Dereference(ext.value))) 170 } 171 } 172 173 a := c.schemas.Elts 174 sort.Slice(a, func(i, j int) bool { 175 x, _, _ := ast.LabelName(a[i].(*ast.Field).Label) 176 y, _, _ := ast.LabelName(a[j].(*ast.Field).Label) 177 return x < y 178 }) 179 180 return (*ast.StructLit)(c.schemas), c.errs 181 } 182 183 func (c *buildContext) build(name string, v cue.Value) *ast.StructLit { 184 return newCoreBuilder(c).schema(nil, name, v) 185 } 186 187 // isInternal reports whether or not to include this type. 188 func (c *buildContext) isInternal(name string) bool { 189 // TODO: allow a regexp filter in Config. If we have closed structs and 190 // definitions, this will likely be unnecessary. 191 return strings.HasSuffix(name, "_value") 192 } 193 194 func (b *builder) failf(v cue.Value, format string, args ...interface{}) { 195 panic(&openapiError{ 196 errors.NewMessage(format, args), 197 b.ctx.path, 198 v.Pos(), 199 }) 200 } 201 202 func (b *builder) unsupported(v cue.Value) { 203 if b.format == "" { 204 // Not strictly an error, but consider listing it as a warning 205 // in strict mode. 206 } 207 } 208 209 func (b *builder) checkArgs(a []cue.Value, n int) { 210 if len(a)-1 != n { 211 b.failf(a[0], "%v must be used with %d arguments", a[0], len(a)-1) 212 } 213 } 214 215 func (b *builder) schema(core *builder, name string, v cue.Value) *ast.StructLit { 216 oldPath := b.ctx.path 217 b.ctx.path = append(b.ctx.path, name) 218 defer func() { b.ctx.path = oldPath }() 219 220 var c *builder 221 if core == nil && b.ctx.structural { 222 c = newCoreBuilder(b.ctx) 223 c.buildCore(v) // initialize core structure 224 c.coreSchema() 225 } else { 226 c = newRootBuilder(b.ctx) 227 c.core = core 228 } 229 230 return c.fillSchema(v) 231 } 232 233 func (b *builder) getDoc(v cue.Value) { 234 doc := []string{} 235 if b.ctx.descFunc != nil { 236 if str := b.ctx.descFunc(v); str != "" { 237 doc = append(doc, str) 238 } 239 } else { 240 for _, d := range v.Doc() { 241 doc = append(doc, d.Text()) 242 } 243 } 244 if len(doc) > 0 { 245 str := strings.TrimSpace(strings.Join(doc, "\n\n")) 246 b.setSingle("description", ast.NewString(str), true) 247 } 248 } 249 250 func (b *builder) fillSchema(v cue.Value) *ast.StructLit { 251 if b.filled != nil { 252 return b.filled 253 } 254 255 b.setValueType(v) 256 b.format = extractFormat(v) 257 b.deprecated = getDeprecated(v) 258 259 if b.core == nil || len(b.core.values) > 1 { 260 isRef := b.value(v, nil) 261 if isRef { 262 b.typ = "" 263 } 264 265 if !isRef && !b.ctx.structural { 266 b.getDoc(v) 267 } 268 } 269 270 schema := b.finish() 271 s := (*ast.StructLit)(schema) 272 273 simplify(b, s) 274 275 sortSchema(s) 276 277 b.filled = s 278 return s 279 } 280 281 func label(d ast.Decl) string { 282 f := d.(*ast.Field) 283 s, _, _ := ast.LabelName(f.Label) 284 return s 285 } 286 287 func value(d ast.Decl) ast.Expr { 288 return d.(*ast.Field).Value 289 } 290 291 func sortSchema(s *ast.StructLit) { 292 sort.Slice(s.Elts, func(i, j int) bool { 293 iName := label(s.Elts[i]) 294 jName := label(s.Elts[j]) 295 pi := fieldOrder[iName] 296 pj := fieldOrder[jName] 297 if pi != pj { 298 return pi > pj 299 } 300 return iName < jName 301 }) 302 } 303 304 var fieldOrder = map[string]int{ 305 "description": 31, 306 "type": 30, 307 "format": 29, 308 "required": 28, 309 "properties": 27, 310 "minProperties": 26, 311 "maxProperties": 25, 312 "minimum": 24, 313 "exclusiveMinimum": 23, 314 "maximum": 22, 315 "exclusiveMaximum": 21, 316 "minItems": 18, 317 "maxItems": 17, 318 "minLength": 16, 319 "maxLength": 15, 320 "items": 14, 321 "enum": 13, 322 "default": 12, 323 } 324 325 func (b *builder) value(v cue.Value, f typeFunc) (isRef bool) { 326 b.pushNode(v) 327 defer b.popNode() 328 329 count := 0 330 disallowDefault := false 331 var values cue.Value 332 if b.ctx.expandRefs || b.format != "" { 333 values = cue.Dereference(v) 334 count = 1 335 } else { 336 dedup := map[string]bool{} 337 hasNoRef := false 338 accept := v 339 conjuncts := appendSplit(nil, cue.AndOp, v) 340 for _, v := range conjuncts { 341 // This may be a reference to an enum. So we need to check references before 342 // dissecting them. 343 switch p, r := v.Reference(); { 344 case len(r) > 0: 345 ref := b.ctx.makeRef(p, r) 346 if ref == "" { 347 v = cue.Dereference(v) 348 break 349 } 350 if dedup[ref] { 351 continue 352 } 353 dedup[ref] = true 354 355 b.addRef(v, p, r) 356 disallowDefault = true 357 continue 358 } 359 hasNoRef = true 360 count++ 361 values = values.UnifyAccept(v, accept) 362 } 363 isRef = !hasNoRef && len(dedup) == 1 364 } 365 366 if count > 0 { // TODO: implement IsAny. 367 // TODO: perhaps find optimal representation. For now we assume the 368 // representation as is is already optimized for human consumption. 369 if values.IncompleteKind()&cue.StructKind != cue.StructKind && !isRef { 370 values = values.Eval() 371 } 372 373 conjuncts := appendSplit(nil, cue.AndOp, values) 374 for i, v := range conjuncts { 375 switch { 376 case isConcrete(v): 377 b.dispatch(f, v) 378 if !b.isNonCore() { 379 b.set("enum", ast.NewList(b.decode(v))) 380 } 381 default: 382 a := appendSplit(nil, cue.OrOp, v) 383 for i, v := range a { 384 if _, r := v.Reference(); len(r) == 0 { 385 a[i] = v.Eval() 386 } 387 } 388 389 _ = i 390 // TODO: it matters here whether a conjunct is obtained 391 // from embedding or normal unification. Fix this at some 392 // point. 393 // 394 // if len(a) > 1 { 395 // // Filter disjuncts that cannot unify with other conjuncts, 396 // // and thus can never be satisfied. 397 // // TODO: there should be generalized simplification logic 398 // // in CUE (outside of the usual implicit simplifications). 399 // k := 0 400 // outer: 401 // for _, d := range a { 402 // for j, w := range conjuncts { 403 // if i == j { 404 // continue 405 // } 406 // if d.Unify(w).Err() != nil { 407 // continue outer 408 // } 409 // } 410 // a[k] = d 411 // k++ 412 // } 413 // a = a[:k] 414 // } 415 switch len(a) { 416 case 0: 417 // Conjunct entirely eliminated. 418 case 1: 419 v = a[0] 420 if err := v.Err(); err != nil { 421 b.failf(v, "openapi: %v", err) 422 return 423 } 424 b.dispatch(f, v) 425 default: 426 b.disjunction(a, f) 427 } 428 } 429 } 430 } 431 432 if v, ok := v.Default(); ok && v.IsConcrete() && !disallowDefault { 433 // TODO: should we show the empty list default? This would be correct 434 // but perhaps a bit too pedantic and noisy. 435 switch { 436 case v.Kind() == cue.ListKind: 437 iter, _ := v.List() 438 if !iter.Next() { 439 // Don't show default for empty list. 440 break 441 } 442 fallthrough 443 default: 444 if !b.isNonCore() { 445 e := v.Syntax(cue.Concrete(true)).(ast.Expr) 446 b.setFilter("Schema", "default", e) 447 } 448 } 449 } 450 return isRef 451 } 452 453 func appendSplit(a []cue.Value, splitBy cue.Op, v cue.Value) []cue.Value { 454 op, args := v.Expr() 455 // dedup elements. 456 k := 1 457 outer: 458 for i := 1; i < len(args); i++ { 459 for j := 0; j < k; j++ { 460 if args[i].Subsume(args[j], cue.Raw()) == nil && 461 args[j].Subsume(args[i], cue.Raw()) == nil { 462 continue outer 463 } 464 } 465 args[k] = args[i] 466 k++ 467 } 468 args = args[:k] 469 470 if op == cue.NoOp && len(args) == 1 { 471 // TODO: this is to deal with default value removal. This may change 472 // whe we completely separate default values from values. 473 a = append(a, args...) 474 } else if op != splitBy { 475 a = append(a, v) 476 } else { 477 for _, v := range args { 478 a = appendSplit(a, splitBy, v) 479 } 480 } 481 return a 482 } 483 484 func countNodes(v cue.Value) (n int) { 485 switch op, a := v.Expr(); op { 486 case cue.OrOp, cue.AndOp: 487 for _, v := range a { 488 n += countNodes(v) 489 } 490 n += len(a) - 1 491 default: 492 switch v.Kind() { 493 case cue.ListKind: 494 for i, _ := v.List(); i.Next(); { 495 n += countNodes(i.Value()) 496 } 497 case cue.StructKind: 498 for i, _ := v.Fields(); i.Next(); { 499 n += countNodes(i.Value()) + 1 500 } 501 } 502 } 503 return n + 1 504 } 505 506 // isConcrete reports whether v is concrete and not a struct (recursively). 507 // structs are not supported as the result of a struct enum depends on how 508 // conjunctions and disjunctions are distributed. We could consider still doing 509 // this if we define a normal form. 510 func isConcrete(v cue.Value) bool { 511 if !v.IsConcrete() { 512 return false 513 } 514 if v.Kind() == cue.StructKind { 515 return false // TODO: handle struct kinds 516 } 517 for list, _ := v.List(); list.Next(); { 518 if !isConcrete(list.Value()) { 519 return false 520 } 521 } 522 return true 523 } 524 525 func (b *builder) disjunction(a []cue.Value, f typeFunc) { 526 disjuncts := []cue.Value{} 527 enums := []ast.Expr{} // TODO: unique the enums 528 nullable := false // Only supported in OpenAPI, not JSON schema 529 530 for _, v := range a { 531 switch { 532 case v.Null() == nil: 533 // TODO: for JSON schema, we need to fall through. 534 nullable = true 535 536 case isConcrete(v): 537 enums = append(enums, b.decode(v)) 538 539 default: 540 disjuncts = append(disjuncts, v) 541 } 542 } 543 544 // Only one conjunct? 545 if len(disjuncts) == 0 || (len(disjuncts) == 1 && len(enums) == 0) { 546 if len(disjuncts) == 1 { 547 b.value(disjuncts[0], f) 548 } 549 if len(enums) > 0 && !b.isNonCore() { 550 b.set("enum", ast.NewList(enums...)) 551 } 552 if nullable { 553 b.setSingle("nullable", ast.NewBool(true), true) // allowed in Structural 554 } 555 return 556 } 557 558 anyOf := []ast.Expr{} 559 if len(enums) > 0 { 560 anyOf = append(anyOf, b.kv("enum", ast.NewList(enums...))) 561 } 562 563 if nullable { 564 b.setSingle("nullable", ast.NewBool(true), true) 565 } 566 567 schemas := make([]*ast.StructLit, len(disjuncts)) 568 for i, v := range disjuncts { 569 c := newOASBuilder(b) 570 c.value(v, f) 571 t := c.finish() 572 schemas[i] = (*ast.StructLit)(t) 573 if len(t.Elts) == 0 { 574 if c.typ == "" { 575 return 576 } 577 } 578 } 579 580 for i, v := range disjuncts { 581 // In OpenAPI schema are open by default. To ensure forward compatibility, 582 // we do not represent closed structs with additionalProperties: false 583 // (this is discouraged and often disallowed by implementions), but 584 // rather enforce this by ensuring uniqueness of the disjuncts. 585 // 586 // TODO: subsumption may currently give false negatives. We are extra 587 // conservative in these instances. 588 subsumed := []ast.Expr{} 589 for j, w := range disjuncts { 590 if i == j { 591 continue 592 } 593 err := v.Subsume(w, cue.Schema()) 594 if err == nil || errors.Is(err, internal.ErrInexact) { 595 subsumed = append(subsumed, schemas[j]) 596 } 597 } 598 599 t := schemas[i] 600 if len(subsumed) > 0 { 601 // TODO: elide anyOf if there is only one element. This should be 602 // rare if originating from oneOf. 603 exclude := ast.NewStruct("not", 604 ast.NewStruct("anyOf", ast.NewList(subsumed...))) 605 if len(t.Elts) == 0 { 606 t = exclude 607 } else { 608 t = ast.NewStruct("allOf", ast.NewList(t, exclude)) 609 } 610 } 611 anyOf = append(anyOf, t) 612 } 613 614 b.set("oneOf", ast.NewList(anyOf...)) 615 } 616 617 func (b *builder) setValueType(v cue.Value) { 618 if b.core != nil { 619 return 620 } 621 622 k := v.IncompleteKind() &^ adt.NullKind 623 switch k { 624 case cue.BoolKind: 625 b.typ = "boolean" 626 case cue.FloatKind, cue.NumberKind: 627 b.typ = "number" 628 case cue.IntKind: 629 b.typ = "integer" 630 case cue.BytesKind: 631 b.typ = "string" 632 case cue.StringKind: 633 b.typ = "string" 634 case cue.StructKind: 635 b.typ = "object" 636 case cue.ListKind: 637 b.typ = "array" 638 } 639 } 640 641 func (b *builder) dispatch(f typeFunc, v cue.Value) { 642 if f != nil { 643 f(b, v) 644 return 645 } 646 647 switch v.IncompleteKind() { 648 case cue.NullKind: 649 // TODO: for JSON schema we would set the type here. For OpenAPI, 650 // it must be nullable. 651 b.setSingle("nullable", ast.NewBool(true), true) 652 653 case cue.BoolKind: 654 b.setType("boolean", "") 655 // No need to call. 656 657 case cue.FloatKind, cue.NumberKind: 658 // TODO: 659 // Common Name type format Comments 660 // float number float 661 // double number double 662 b.setType("number", "") // may be overridden to integer 663 b.number(v) 664 665 case cue.IntKind: 666 // integer integer int32 signed 32 bits 667 // long integer int64 signed 64 bits 668 b.setType("integer", "") // may be overridden to integer 669 b.number(v) 670 671 // TODO: for JSON schema, consider adding multipleOf: 1. 672 673 case cue.BytesKind: 674 // byte string byte base64 encoded characters 675 // binary string binary any sequence of octets 676 b.setType("string", "byte") 677 b.bytes(v) 678 case cue.StringKind: 679 // date string date As defined by full-date - RFC3339 680 // dateTime string date-time As defined by date-time - RFC3339 681 // password string password A hint to UIs to obscure input 682 b.setType("string", "") 683 b.string(v) 684 case cue.StructKind: 685 b.setType("object", "") 686 b.object(v) 687 case cue.ListKind: 688 b.setType("array", "") 689 b.array(v) 690 } 691 } 692 693 // object supports the following 694 // - maxProperties: maximum allowed fields in this struct. 695 // - minProperties: minimum required fields in this struct. 696 // - patternProperties: [regexp]: schema 697 // TODO: we can support this once .kv(key, value) allow 698 // foo [=~"pattern"]: type 699 // An instance field must match all schemas for which a regexp matches. 700 // Even though it is not supported in OpenAPI, we should still accept it 701 // when receiving from OpenAPI. We could possibly use disjunctions to encode 702 // this. 703 // - dependencies: what? 704 // - propertyNames: schema 705 // every property name in the enclosed schema matches that of 706 func (b *builder) object(v cue.Value) { 707 // TODO: discriminator objects: we could theoretically derive discriminator 708 // objects automatically: for every object in a oneOf/allOf/anyOf, or any 709 // object composed of the same type, if a property is required and set to a 710 // constant value for each type, it is a discriminator. 711 712 switch op, a := v.Expr(); op { 713 case cue.CallOp: 714 name := fmt.Sprint(a[0]) 715 switch name { 716 case "struct.MinFields": 717 b.checkArgs(a, 1) 718 b.setFilter("Schema", "minProperties", b.int(a[1])) 719 return 720 721 case "struct.MaxFields": 722 b.checkArgs(a, 1) 723 b.setFilter("Schema", "maxProperties", b.int(a[1])) 724 return 725 726 default: 727 b.unsupported(a[0]) 728 return 729 } 730 731 case cue.NoOp: 732 // TODO: extract format from specific type. 733 734 default: 735 b.failf(v, "unsupported op %v for object type (%v)", op, v) 736 return 737 } 738 739 required := []ast.Expr{} 740 for i, _ := v.Fields(); i.Next(); { 741 required = append(required, ast.NewString(i.Label())) 742 } 743 if len(required) > 0 { 744 b.setFilter("Schema", "required", ast.NewList(required...)) 745 } 746 747 var properties *OrderedMap 748 if b.singleFields != nil { 749 properties = b.singleFields.getMap("properties") 750 } 751 hasProps := properties != nil 752 if !hasProps { 753 properties = &OrderedMap{} 754 } 755 756 for i, _ := v.Fields(cue.Optional(true), cue.Definitions(true)); i.Next(); { 757 label := i.Label() 758 if b.ctx.isInternal(label) { 759 continue 760 } 761 if i.IsDefinition() && strings.HasPrefix(label, "#") { 762 label = label[1:] 763 } 764 var core *builder 765 if b.core != nil { 766 core = b.core.properties[label] 767 } 768 schema := b.schema(core, label, i.Value()) 769 switch { 770 case i.IsDefinition(): 771 ref := b.ctx.makeRef(b.ctx.instExt, append(b.ctx.path, label)) 772 if ref == "" { 773 continue 774 } 775 b.ctx.schemas.Set(ref, schema) 776 case !b.isNonCore() || len(schema.Elts) > 0: 777 properties.Set(label, schema) 778 } 779 } 780 781 if !hasProps && properties.len() > 0 { 782 b.setSingle("properties", (*ast.StructLit)(properties), false) 783 } 784 785 if t, ok := v.Elem(); ok && 786 (b.core == nil || b.core.items == nil) && b.checkCycle(t) { 787 schema := b.schema(nil, "*", t) 788 if len(schema.Elts) > 0 { 789 b.setSingle("additionalProperties", schema, true) // Not allowed in structural. 790 } 791 } 792 793 // TODO: maxProperties, minProperties: can be done once we allow cap to 794 // unify with structs. 795 } 796 797 // List constraints: 798 // 799 // Max and min items. 800 // - maxItems: int (inclusive) 801 // - minItems: int (inclusive) 802 // - items (item type) 803 // schema: applies to all items 804 // array of schemas: 805 // schema at pos must match if both value and items are defined. 806 // - additional items: 807 // schema: where items must be an array of schemas, intstance elements 808 // succeed for if they match this value for any value at a position 809 // greater than that covered by items. 810 // - uniqueItems: bool 811 // TODO: support with list.Unique() unique() or comprehensions. 812 // For the latter, we need equality for all values, which is doable, 813 // but not done yet. 814 // 815 // NOT SUPPORTED IN OpenAPI: 816 // - contains: 817 // schema: an array instance is valid if at least one element matches 818 // this schema. 819 func (b *builder) array(v cue.Value) { 820 821 switch op, a := v.Expr(); op { 822 case cue.CallOp: 823 name := fmt.Sprint(a[0]) 824 switch name { 825 case "list.UniqueItems", "list.UniqueItems()": 826 b.checkArgs(a, 0) 827 b.setFilter("Schema", "uniqueItems", ast.NewBool(true)) 828 return 829 830 case "list.MinItems": 831 b.checkArgs(a, 1) 832 b.setFilter("Schema", "minItems", b.int(a[1])) 833 return 834 835 case "list.MaxItems": 836 b.checkArgs(a, 1) 837 b.setFilter("Schema", "maxItems", b.int(a[1])) 838 return 839 840 default: 841 b.unsupported(a[0]) 842 return 843 } 844 845 case cue.NoOp: 846 // TODO: extract format from specific type. 847 848 default: 849 b.failf(v, "unsupported op %v for array type", op) 850 return 851 } 852 853 // Possible conjuncts: 854 // - one list (CUE guarantees merging all conjuncts) 855 // - no cap: is unified with list 856 // - unique items: at most one, but idempotent if multiple. 857 // There is never a need for allOf or anyOf. Note that a CUE list 858 // corresponds almost one-to-one to OpenAPI lists. 859 items := []ast.Expr{} 860 count := 0 861 for i, _ := v.List(); i.Next(); count++ { 862 items = append(items, b.schema(nil, strconv.Itoa(count), i.Value())) 863 } 864 if len(items) > 0 { 865 // TODO: per-item schema are not allowed in OpenAPI, only in JSON Schema. 866 // Perhaps we should turn this into an OR after first normalizing 867 // the entries. 868 b.set("items", ast.NewList(items...)) 869 // panic("per-item types not supported in OpenAPI") 870 } 871 872 // TODO: 873 // A CUE cap can be a set of discontinuous ranges. If we encounter this, 874 // we can create an allOf(list type, anyOf(ranges)). 875 cap := v.Len() 876 hasMax := false 877 maxLength := int64(math.MaxInt64) 878 879 if n, capErr := cap.Int64(); capErr == nil { 880 maxLength = n 881 hasMax = true 882 } else { 883 b.value(cap, (*builder).listCap) 884 } 885 886 if !hasMax || int64(len(items)) < maxLength { 887 if typ, ok := v.Elem(); ok && b.checkCycle(typ) { 888 var core *builder 889 if b.core != nil { 890 core = b.core.items 891 } 892 t := b.schema(core, "*", typ) 893 if len(items) > 0 { 894 b.setFilter("Schema", "additionalItems", t) // Not allowed in structural. 895 } else if !b.isNonCore() || len(t.Elts) > 0 { 896 b.setSingle("items", t, true) 897 } 898 } 899 } 900 } 901 902 func (b *builder) listCap(v cue.Value) { 903 switch op, a := v.Expr(); op { 904 case cue.LessThanOp: 905 b.setFilter("Schema", "maxItems", b.inta(a[0], -1)) 906 case cue.LessThanEqualOp: 907 b.setFilter("Schema", "maxItems", b.inta(a[0], 0)) 908 case cue.GreaterThanOp: 909 b.setFilter("Schema", "minItems", b.inta(a[0], 1)) 910 case cue.GreaterThanEqualOp: 911 if b.int64(a[0]) > 0 { 912 b.setFilter("Schema", "minItems", b.inta(a[0], 0)) 913 } 914 case cue.NoOp: 915 // must be type, so okay. 916 case cue.NotEqualOp: 917 i := b.int(a[0]) 918 b.setNot("allOff", ast.NewList( 919 b.kv("minItems", i), 920 b.kv("maxItems", i), 921 )) 922 923 default: 924 b.failf(v, "unsupported op for list capacity %v", op) 925 return 926 } 927 } 928 929 func (b *builder) number(v cue.Value) { 930 // Multiple conjuncts mostly means just additive constraints. 931 // Type may be number of float. 932 933 switch op, a := v.Expr(); op { 934 case cue.LessThanOp: 935 if b.ctx.exclusiveBool { 936 b.setFilter("Schema", "exclusiveMaximum", ast.NewBool(true)) 937 b.setFilter("Schema", "maximum", b.big(a[0])) 938 } else { 939 b.setFilter("Schema", "exclusiveMaximum", b.big(a[0])) 940 } 941 942 case cue.LessThanEqualOp: 943 b.setFilter("Schema", "maximum", b.big(a[0])) 944 945 case cue.GreaterThanOp: 946 if b.ctx.exclusiveBool { 947 b.setFilter("Schema", "exclusiveMinimum", ast.NewBool(true)) 948 b.setFilter("Schema", "minimum", b.big(a[0])) 949 } else { 950 b.setFilter("Schema", "exclusiveMinimum", b.big(a[0])) 951 } 952 953 case cue.GreaterThanEqualOp: 954 b.setFilter("Schema", "minimum", b.big(a[0])) 955 956 case cue.NotEqualOp: 957 i := b.big(a[0]) 958 b.setNot("allOff", ast.NewList( 959 b.kv("minimum", i), 960 b.kv("maximum", i), 961 )) 962 963 case cue.CallOp: 964 name := fmt.Sprint(a[0]) 965 switch name { 966 case "math.MultipleOf": 967 b.checkArgs(a, 1) 968 b.setFilter("Schema", "multipleOf", b.int(a[1])) 969 970 default: 971 b.unsupported(a[0]) 972 return 973 } 974 975 case cue.NoOp: 976 // TODO: extract format from specific type. 977 978 default: 979 b.failf(v, "unsupported op for number %v", op) 980 } 981 } 982 983 // Multiple Regexp conjuncts are represented as allOf all other 984 // constraints can be combined unless in the even of discontinuous 985 // lengths. 986 987 // string supports the following options: 988 // 989 // - maxLength (Unicode codepoints) 990 // - minLength (Unicode codepoints) 991 // - pattern (a regexp) 992 // 993 // The regexp pattern is as follows, and is limited to be a strict subset of RE2: 994 // Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-01#section-3.3 995 // 996 // JSON schema requires ECMA 262 regular expressions, but 997 // limited to the following constructs: 998 // - simple character classes: [abc] 999 // - range character classes: [a-z] 1000 // - complement character classes: [^abc], [^a-z] 1001 // - simple quantifiers: +, *, ?, and lazy versions +? *? ?? 1002 // - range quantifiers: {x}, {x,y}, {x,}, {x}?, {x,y}?, {x,}? 1003 // - begin and end anchors: ^ and $ 1004 // - simple grouping: (...) 1005 // - alteration: | 1006 // This is a subset of RE2 used by CUE. 1007 // 1008 // Most notably absent: 1009 // - the '.' for any character (not sure if that is a doc bug) 1010 // - character classes \d \D [[::]] \pN \p{Name} \PN \P{Name} 1011 // - word boundaries 1012 // - capturing directives. 1013 // - flag setting 1014 // - comments 1015 // 1016 // The capturing directives and comments can be removed without 1017 // compromising the meaning of the regexp (TODO). Removing 1018 // flag setting will be tricky. Unicode character classes, 1019 // boundaries, etc can be compiled into simple character classes, 1020 // although the resulting regexp will look cumbersome. 1021 // 1022 func (b *builder) string(v cue.Value) { 1023 switch op, a := v.Expr(); op { 1024 1025 case cue.RegexMatchOp, cue.NotRegexMatchOp: 1026 s, err := a[0].String() 1027 if err != nil { 1028 // TODO: this may be an unresolved interpolation or expression. Consider 1029 // whether it is reasonable to treat unevaluated operands as wholes and 1030 // generate a compound regular expression. 1031 b.failf(v, "regexp value must be a string: %v", err) 1032 return 1033 } 1034 if op == cue.RegexMatchOp { 1035 b.setFilter("Schema", "pattern", ast.NewString(s)) 1036 } else { 1037 b.setNot("pattern", ast.NewString(s)) 1038 } 1039 1040 case cue.NoOp, cue.SelectorOp: 1041 1042 case cue.CallOp: 1043 name := fmt.Sprint(a[0]) 1044 switch name { 1045 case "strings.MinRunes": 1046 b.checkArgs(a, 1) 1047 b.setFilter("Schema", "minLength", b.int(a[1])) 1048 return 1049 1050 case "strings.MaxRunes": 1051 b.checkArgs(a, 1) 1052 b.setFilter("Schema", "maxLength", b.int(a[1])) 1053 return 1054 1055 default: 1056 b.unsupported(a[0]) 1057 return 1058 } 1059 1060 default: 1061 b.failf(v, "unsupported op %v for string type", op) 1062 } 1063 } 1064 1065 func (b *builder) bytes(v cue.Value) { 1066 switch op, a := v.Expr(); op { 1067 1068 case cue.RegexMatchOp, cue.NotRegexMatchOp: 1069 s, err := a[0].Bytes() 1070 if err != nil { 1071 // TODO: this may be an unresolved interpolation or expression. Consider 1072 // whether it is reasonable to treat unevaluated operands as wholes and 1073 // generate a compound regular expression. 1074 b.failf(v, "regexp value must be of type bytes: %v", err) 1075 return 1076 } 1077 1078 e := ast.NewString(string(s)) 1079 if op == cue.RegexMatchOp { 1080 b.setFilter("Schema", "pattern", e) 1081 } else { 1082 b.setNot("pattern", e) 1083 } 1084 1085 // TODO: support the following JSON schema constraints 1086 // - maxLength 1087 // - minLength 1088 1089 case cue.NoOp, cue.SelectorOp: 1090 1091 default: 1092 b.failf(v, "unsupported op %v for bytes type", op) 1093 } 1094 } 1095 1096 type builder struct { 1097 ctx *buildContext 1098 typ string 1099 format string 1100 singleFields *oaSchema 1101 current *oaSchema 1102 allOf []*ast.StructLit 1103 deprecated bool 1104 1105 // Building structural schema 1106 core *builder 1107 kind cue.Kind 1108 filled *ast.StructLit 1109 values []cue.Value // in structural mode, all values of not and *Of. 1110 keys []string 1111 properties map[string]*builder 1112 items *builder 1113 } 1114 1115 func newRootBuilder(c *buildContext) *builder { 1116 return &builder{ctx: c} 1117 } 1118 1119 func newOASBuilder(parent *builder) *builder { 1120 core := parent 1121 if parent.core != nil { 1122 core = parent.core 1123 } 1124 b := &builder{ 1125 core: core, 1126 ctx: parent.ctx, 1127 typ: parent.typ, 1128 format: parent.format, 1129 } 1130 return b 1131 } 1132 1133 func (b *builder) isNonCore() bool { 1134 return b.core != nil 1135 } 1136 1137 func (b *builder) setType(t, format string) { 1138 if b.typ == "" { 1139 b.typ = t 1140 if format != "" { 1141 b.format = format 1142 } 1143 } 1144 } 1145 1146 func setType(t *oaSchema, b *builder) { 1147 if b.typ != "" { 1148 if b.core == nil || (b.core.typ != b.typ && !b.ctx.structural) { 1149 if !t.exists("type") { 1150 t.Set("type", ast.NewString(b.typ)) 1151 } 1152 } 1153 } 1154 if b.format != "" { 1155 if b.core == nil || b.core.format != b.format { 1156 t.Set("format", ast.NewString(b.format)) 1157 } 1158 } 1159 } 1160 1161 // setFilter is like set, but allows the key-value pair to be filtered. 1162 func (b *builder) setFilter(schema, key string, v ast.Expr) { 1163 if re := b.ctx.fieldFilter; re != nil && re.MatchString(path.Join(schema, key)) { 1164 return 1165 } 1166 b.set(key, v) 1167 } 1168 1169 // setSingle sets a value of which there should only be one. 1170 func (b *builder) setSingle(key string, v ast.Expr, drop bool) { 1171 if b.singleFields == nil { 1172 b.singleFields = &OrderedMap{} 1173 } 1174 if b.singleFields.exists(key) { 1175 if !drop { 1176 b.failf(cue.Value{}, "more than one value added for key %q", key) 1177 } 1178 } 1179 b.singleFields.Set(key, v) 1180 } 1181 1182 func (b *builder) set(key string, v ast.Expr) { 1183 if b.current == nil { 1184 b.current = &OrderedMap{} 1185 b.allOf = append(b.allOf, (*ast.StructLit)(b.current)) 1186 } else if b.current.exists(key) { 1187 b.current = &OrderedMap{} 1188 b.allOf = append(b.allOf, (*ast.StructLit)(b.current)) 1189 } 1190 b.current.Set(key, v) 1191 } 1192 1193 func (b *builder) kv(key string, value ast.Expr) *ast.StructLit { 1194 return ast.NewStruct(key, value) 1195 } 1196 1197 func (b *builder) setNot(key string, value ast.Expr) { 1198 b.add(ast.NewStruct("not", b.kv(key, value))) 1199 } 1200 1201 func (b *builder) finish() *ast.StructLit { 1202 var t *OrderedMap 1203 1204 if b.filled != nil { 1205 return b.filled 1206 } 1207 switch len(b.allOf) { 1208 case 0: 1209 t = &OrderedMap{} 1210 1211 case 1: 1212 hasRef := false 1213 for _, e := range b.allOf[0].Elts { 1214 if f, ok := e.(*ast.Field); ok { 1215 name, _, _ := ast.LabelName(f.Label) 1216 hasRef = hasRef || name == "$ref" 1217 } 1218 } 1219 if !hasRef || b.singleFields == nil { 1220 t = (*OrderedMap)(b.allOf[0]) 1221 break 1222 } 1223 fallthrough 1224 1225 default: 1226 exprs := []ast.Expr{} 1227 if t != nil { 1228 exprs = append(exprs, (*ast.StructLit)(t)) 1229 } 1230 for _, s := range b.allOf { 1231 exprs = append(exprs, s) 1232 } 1233 t = &OrderedMap{} 1234 t.Set("allOf", ast.NewList(exprs...)) 1235 } 1236 if b.singleFields != nil { 1237 b.singleFields.Elts = append(b.singleFields.Elts, t.Elts...) 1238 t = b.singleFields 1239 } 1240 if b.deprecated { 1241 t.Set("deprecated", ast.NewBool(true)) 1242 } 1243 setType(t, b) 1244 sortSchema((*ast.StructLit)(t)) 1245 return (*ast.StructLit)(t) 1246 } 1247 1248 func (b *builder) add(t *ast.StructLit) { 1249 b.allOf = append(b.allOf, t) 1250 } 1251 1252 func (b *builder) addConjunct(f func(*builder)) { 1253 c := newOASBuilder(b) 1254 f(c) 1255 b.add((*ast.StructLit)(c.finish())) 1256 } 1257 1258 func (b *builder) addRef(v cue.Value, inst *cue.Instance, ref []string) { 1259 name := b.ctx.makeRef(inst, ref) 1260 b.addConjunct(func(b *builder) { 1261 b.allOf = append(b.allOf, ast.NewStruct( 1262 "$ref", ast.NewString(path.Join("#", b.ctx.refPrefix, name)))) 1263 }) 1264 1265 if b.ctx.inst != inst { 1266 b.ctx.externalRefs[name] = &externalType{ 1267 ref: name, 1268 inst: inst, 1269 path: ref, 1270 value: v, 1271 } 1272 } 1273 } 1274 1275 func (b *buildContext) makeRef(inst *cue.Instance, ref []string) string { 1276 ref = append([]string{}, ref...) 1277 for i, s := range ref { 1278 if strings.HasPrefix(s, "#") { 1279 ref[i] = s[1:] 1280 } 1281 } 1282 a := make([]string, 0, len(ref)+3) 1283 if b.nameFunc != nil { 1284 a = append(a, b.nameFunc(inst, ref)) 1285 } else { 1286 a = append(a, ref...) 1287 } 1288 return strings.Join(a, ".") 1289 } 1290 1291 func (b *builder) int64(v cue.Value) int64 { 1292 v, _ = v.Default() 1293 i, err := v.Int64() 1294 if err != nil { 1295 b.failf(v, "could not retrieve int: %v", err) 1296 } 1297 return i 1298 } 1299 1300 func (b *builder) intExpr(i int64) ast.Expr { 1301 return &ast.BasicLit{ 1302 Kind: token.INT, 1303 Value: fmt.Sprint(i), 1304 } 1305 } 1306 1307 func (b *builder) int(v cue.Value) ast.Expr { 1308 return b.intExpr(b.int64(v)) 1309 } 1310 1311 func (b *builder) inta(v cue.Value, offset int64) ast.Expr { 1312 return b.intExpr(b.int64(v) + offset) 1313 } 1314 1315 func (b *builder) decode(v cue.Value) ast.Expr { 1316 v, _ = v.Default() 1317 return v.Syntax(cue.Final()).(ast.Expr) 1318 } 1319 1320 func (b *builder) big(v cue.Value) ast.Expr { 1321 v, _ = v.Default() 1322 return v.Syntax(cue.Final()).(ast.Expr) 1323 }