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