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