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