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}