| // Copyright 2019 CUE Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package openapi |
| |
| import ( |
| "fmt" |
| "math" |
| "path" |
| "regexp" |
| "sort" |
| "strconv" |
| "strings" |
| |
| "cuelang.org/go/cue" |
| "cuelang.org/go/cue/ast" |
| "cuelang.org/go/cue/errors" |
| "cuelang.org/go/cue/token" |
| "cuelang.org/go/internal" |
| "cuelang.org/go/internal/core/adt" |
| ) |
| |
| type buildContext struct { |
| inst *cue.Instance |
| instExt *cue.Instance |
| refPrefix string |
| path []string |
| |
| expandRefs bool |
| structural bool |
| exclusiveBool bool |
| nameFunc func(inst *cue.Instance, path []string) string |
| descFunc func(v cue.Value) string |
| fieldFilter *regexp.Regexp |
| evalDepth int // detect cycles when resolving references |
| |
| schemas *OrderedMap |
| |
| // Track external schemas. |
| externalRefs map[string]*externalType |
| } |
| |
| type externalType struct { |
| ref string |
| inst *cue.Instance |
| path []string |
| value cue.Value |
| } |
| |
| type oaSchema = OrderedMap |
| |
| type typeFunc func(b *builder, a cue.Value) |
| |
| func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err error) { |
| var fieldFilter *regexp.Regexp |
| if g.FieldFilter != "" { |
| fieldFilter, err = regexp.Compile(g.FieldFilter) |
| if err != nil { |
| return nil, errors.Newf(token.NoPos, "invalid field filter: %v", err) |
| } |
| |
| // verify that certain elements are still passed. |
| for _, f := range strings.Split( |
| "version,title,allOf,anyOf,not,enum,Schema/properties,Schema/items"+ |
| "nullable,type", ",") { |
| if fieldFilter.MatchString(f) { |
| return nil, errors.Newf(token.NoPos, "field filter may not exclude %q", f) |
| } |
| } |
| } |
| |
| if g.Version == "" { |
| g.Version = "3.0.0" |
| } |
| |
| c := buildContext{ |
| inst: inst, |
| instExt: inst, |
| refPrefix: "components/schemas", |
| expandRefs: g.ExpandReferences, |
| structural: g.ExpandReferences, |
| nameFunc: g.ReferenceFunc, |
| descFunc: g.DescriptionFunc, |
| schemas: &OrderedMap{}, |
| externalRefs: map[string]*externalType{}, |
| fieldFilter: fieldFilter, |
| } |
| |
| switch g.Version { |
| case "3.0.0": |
| c.exclusiveBool = true |
| case "3.1.0": |
| default: |
| return nil, errors.Newf(token.NoPos, "unsupported version %s", g.Version) |
| } |
| |
| defer func() { |
| switch x := recover().(type) { |
| case nil: |
| case *openapiError: |
| err = x |
| default: |
| panic(x) |
| } |
| }() |
| |
| // Although paths is empty for now, it makes it valid OpenAPI spec. |
| |
| i, err := inst.Value().Fields(cue.Definitions(true)) |
| if err != nil { |
| return nil, err |
| } |
| for i.Next() { |
| if !i.IsDefinition() { |
| continue |
| } |
| // message, enum, or constant. |
| label := i.Label() |
| if c.isInternal(label) { |
| continue |
| } |
| if i.IsDefinition() && strings.HasPrefix(label, "#") { |
| label = label[1:] |
| } |
| ref := c.makeRef(inst, []string{label}) |
| if ref == "" { |
| continue |
| } |
| c.schemas.Set(ref, c.build(label, i.Value())) |
| } |
| |
| // keep looping until a fixed point is reached. |
| for done := 0; len(c.externalRefs) != done; { |
| done = len(c.externalRefs) |
| |
| // From now on, all references need to be expanded |
| external := []string{} |
| for k := range c.externalRefs { |
| external = append(external, k) |
| } |
| sort.Strings(external) |
| |
| for _, k := range external { |
| ext := c.externalRefs[k] |
| c.instExt = ext.inst |
| last := len(ext.path) - 1 |
| c.path = ext.path[:last] |
| name := ext.path[last] |
| c.schemas.Set(ext.ref, c.build(name, cue.Dereference(ext.value))) |
| } |
| } |
| |
| a := c.schemas.Elts |
| sort.Slice(a, func(i, j int) bool { |
| x, _, _ := ast.LabelName(a[i].(*ast.Field).Label) |
| y, _, _ := ast.LabelName(a[j].(*ast.Field).Label) |
| return x < y |
| }) |
| |
| return (*ast.StructLit)(c.schemas), nil |
| } |
| |
| func (c *buildContext) build(name string, v cue.Value) *ast.StructLit { |
| return newCoreBuilder(c).schema(nil, name, v) |
| } |
| |
| // isInternal reports whether or not to include this type. |
| func (c *buildContext) isInternal(name string) bool { |
| // TODO: allow a regexp filter in Config. If we have closed structs and |
| // definitions, this will likely be unnecessary. |
| return strings.HasSuffix(name, "_value") |
| } |
| |
| func (b *builder) failf(v cue.Value, format string, args ...interface{}) { |
| panic(&openapiError{ |
| errors.NewMessage(format, args), |
| b.ctx.path, |
| v.Pos(), |
| }) |
| } |
| |
| func (b *builder) unsupported(v cue.Value) { |
| if b.format == "" { |
| // Not strictly an error, but consider listing it as a warning |
| // in strict mode. |
| } |
| } |
| |
| func (b *builder) checkArgs(a []cue.Value, n int) { |
| if len(a)-1 != n { |
| b.failf(a[0], "%v must be used with %d arguments", a[0], len(a)-1) |
| } |
| } |
| |
| func (b *builder) schema(core *builder, name string, v cue.Value) *ast.StructLit { |
| oldPath := b.ctx.path |
| b.ctx.path = append(b.ctx.path, name) |
| defer func() { b.ctx.path = oldPath }() |
| |
| var c *builder |
| if core == nil && b.ctx.structural { |
| c = newCoreBuilder(b.ctx) |
| c.buildCore(v) // initialize core structure |
| c.coreSchema() |
| } else { |
| c = newRootBuilder(b.ctx) |
| c.core = core |
| } |
| |
| return c.fillSchema(v) |
| } |
| |
| func (b *builder) getDoc(v cue.Value) { |
| doc := []string{} |
| if b.ctx.descFunc != nil { |
| if str := b.ctx.descFunc(v); str != "" { |
| doc = append(doc, str) |
| } |
| } else { |
| for _, d := range v.Doc() { |
| doc = append(doc, d.Text()) |
| } |
| } |
| if len(doc) > 0 { |
| str := strings.TrimSpace(strings.Join(doc, "\n\n")) |
| b.setSingle("description", ast.NewString(str), true) |
| } |
| } |
| |
| func (b *builder) fillSchema(v cue.Value) *ast.StructLit { |
| if b.filled != nil { |
| return b.filled |
| } |
| |
| b.setValueType(v) |
| b.format = extractFormat(v) |
| b.deprecated = getDeprecated(v) |
| |
| if b.core == nil || len(b.core.values) > 1 { |
| isRef := b.value(v, nil) |
| if isRef { |
| b.typ = "" |
| } |
| |
| if !isRef && !b.ctx.structural { |
| b.getDoc(v) |
| } |
| } |
| |
| schema := b.finish() |
| s := (*ast.StructLit)(schema) |
| |
| simplify(b, s) |
| |
| sortSchema(s) |
| |
| b.filled = s |
| return s |
| } |
| |
| func label(d ast.Decl) string { |
| f := d.(*ast.Field) |
| s, _, _ := ast.LabelName(f.Label) |
| return s |
| } |
| |
| func value(d ast.Decl) ast.Expr { |
| return d.(*ast.Field).Value |
| } |
| |
| func sortSchema(s *ast.StructLit) { |
| sort.Slice(s.Elts, func(i, j int) bool { |
| iName := label(s.Elts[i]) |
| jName := label(s.Elts[j]) |
| pi := fieldOrder[iName] |
| pj := fieldOrder[jName] |
| if pi != pj { |
| return pi > pj |
| } |
| return iName < jName |
| }) |
| } |
| |
| var fieldOrder = map[string]int{ |
| "description": 31, |
| "type": 30, |
| "format": 29, |
| "required": 28, |
| "properties": 27, |
| "minProperties": 26, |
| "maxProperties": 25, |
| "minimum": 24, |
| "exclusiveMinimum": 23, |
| "maximum": 22, |
| "exclusiveMaximum": 21, |
| "minItems": 18, |
| "maxItems": 17, |
| "minLength": 16, |
| "maxLength": 15, |
| "items": 14, |
| "enum": 13, |
| "default": 12, |
| } |
| |
| func (b *builder) value(v cue.Value, f typeFunc) (isRef bool) { |
| count := 0 |
| disallowDefault := false |
| var values cue.Value |
| if b.ctx.expandRefs || b.format != "" { |
| values = cue.Dereference(v) |
| count = 1 |
| } else { |
| dedup := map[string]bool{} |
| hasNoRef := false |
| accept := v |
| conjuncts := appendSplit(nil, cue.AndOp, v) |
| for _, v := range conjuncts { |
| // This may be a reference to an enum. So we need to check references before |
| // dissecting them. |
| switch p, r := v.Reference(); { |
| case len(r) > 0: |
| ref := b.ctx.makeRef(p, r) |
| if ref == "" { |
| v = cue.Dereference(v) |
| break |
| } |
| if dedup[ref] { |
| continue |
| } |
| dedup[ref] = true |
| |
| b.addRef(v, p, r) |
| disallowDefault = true |
| continue |
| } |
| hasNoRef = true |
| count++ |
| values = values.UnifyAccept(v, accept) |
| } |
| isRef = !hasNoRef && len(dedup) == 1 |
| } |
| |
| if count > 0 { // TODO: implement IsAny. |
| // TODO: perhaps find optimal representation. For now we assume the |
| // representation as is is already optimized for human consumption. |
| if values.IncompleteKind()&cue.StructKind != cue.StructKind && !isRef { |
| values = values.Eval() |
| } |
| |
| conjuncts := appendSplit(nil, cue.AndOp, values) |
| for i, v := range conjuncts { |
| switch { |
| case isConcrete(v): |
| b.dispatch(f, v) |
| if !b.isNonCore() { |
| b.set("enum", ast.NewList(b.decode(v))) |
| } |
| default: |
| a := appendSplit(nil, cue.OrOp, v) |
| for i, v := range a { |
| if _, r := v.Reference(); len(r) == 0 { |
| a[i] = v.Eval() |
| } |
| } |
| |
| _ = i |
| // TODO: it matters here whether a conjunct is obtained |
| // from embedding or normal unification. Fix this at some |
| // point. |
| // |
| // if len(a) > 1 { |
| // // Filter disjuncts that cannot unify with other conjuncts, |
| // // and thus can never be satisfied. |
| // // TODO: there should be generalized simplification logic |
| // // in CUE (outside of the usual implicit simplifications). |
| // k := 0 |
| // outer: |
| // for _, d := range a { |
| // for j, w := range conjuncts { |
| // if i == j { |
| // continue |
| // } |
| // if d.Unify(w).Err() != nil { |
| // continue outer |
| // } |
| // } |
| // a[k] = d |
| // k++ |
| // } |
| // a = a[:k] |
| // } |
| switch len(a) { |
| case 0: |
| // Conjunct entirely eliminated. |
| case 1: |
| v = a[0] |
| if err := v.Err(); err != nil { |
| b.failf(v, "openapi: %v", err) |
| return |
| } |
| b.dispatch(f, v) |
| default: |
| b.disjunction(a, f) |
| } |
| } |
| } |
| } |
| |
| if v, ok := v.Default(); ok && v.IsConcrete() && !disallowDefault { |
| // TODO: should we show the empty list default? This would be correct |
| // but perhaps a bit too pedantic and noisy. |
| switch { |
| case v.Kind() == cue.ListKind: |
| iter, _ := v.List() |
| if !iter.Next() { |
| // Don't show default for empty list. |
| break |
| } |
| fallthrough |
| default: |
| if !b.isNonCore() { |
| e := v.Syntax(cue.Concrete(true)).(ast.Expr) |
| b.setFilter("Schema", "default", e) |
| } |
| } |
| } |
| return isRef |
| } |
| |
| func appendSplit(a []cue.Value, splitBy cue.Op, v cue.Value) []cue.Value { |
| op, args := v.Expr() |
| // dedup elements. |
| k := 1 |
| outer: |
| for i := 1; i < len(args); i++ { |
| for j := 0; j < k; j++ { |
| if args[i].Subsume(args[j], cue.Raw()) == nil && |
| args[j].Subsume(args[i], cue.Raw()) == nil { |
| continue outer |
| } |
| } |
| args[k] = args[i] |
| k++ |
| } |
| args = args[:k] |
| |
| if op == cue.NoOp && len(args) == 1 { |
| // TODO: this is to deal with default value removal. This may change |
| // whe we completely separate default values from values. |
| a = append(a, args...) |
| } else if op != splitBy { |
| a = append(a, v) |
| } else { |
| for _, v := range args { |
| a = appendSplit(a, splitBy, v) |
| } |
| } |
| return a |
| } |
| |
| func countNodes(v cue.Value) (n int) { |
| switch op, a := v.Expr(); op { |
| case cue.OrOp, cue.AndOp: |
| for _, v := range a { |
| n += countNodes(v) |
| } |
| n += len(a) - 1 |
| default: |
| switch v.Kind() { |
| case cue.ListKind: |
| for i, _ := v.List(); i.Next(); { |
| n += countNodes(i.Value()) |
| } |
| case cue.StructKind: |
| for i, _ := v.Fields(); i.Next(); { |
| n += countNodes(i.Value()) + 1 |
| } |
| } |
| } |
| return n + 1 |
| } |
| |
| // isConcrete reports whether v is concrete and not a struct (recursively). |
| // structs are not supported as the result of a struct enum depends on how |
| // conjunctions and disjunctions are distributed. We could consider still doing |
| // this if we define a normal form. |
| func isConcrete(v cue.Value) bool { |
| if !v.IsConcrete() { |
| return false |
| } |
| if v.Kind() == cue.StructKind { |
| return false // TODO: handle struct kinds |
| } |
| for list, _ := v.List(); list.Next(); { |
| if !isConcrete(list.Value()) { |
| return false |
| } |
| } |
| return true |
| } |
| |
| func (b *builder) disjunction(a []cue.Value, f typeFunc) { |
| disjuncts := []cue.Value{} |
| enums := []ast.Expr{} // TODO: unique the enums |
| nullable := false // Only supported in OpenAPI, not JSON schema |
| |
| for _, v := range a { |
| switch { |
| case v.Null() == nil: |
| // TODO: for JSON schema, we need to fall through. |
| nullable = true |
| |
| case isConcrete(v): |
| enums = append(enums, b.decode(v)) |
| |
| default: |
| disjuncts = append(disjuncts, v) |
| } |
| } |
| |
| // Only one conjunct? |
| if len(disjuncts) == 0 || (len(disjuncts) == 1 && len(enums) == 0) { |
| if len(disjuncts) == 1 { |
| b.value(disjuncts[0], f) |
| } |
| if len(enums) > 0 && !b.isNonCore() { |
| b.set("enum", ast.NewList(enums...)) |
| } |
| if nullable { |
| b.setSingle("nullable", ast.NewBool(true), true) // allowed in Structural |
| } |
| return |
| } |
| |
| anyOf := []ast.Expr{} |
| if len(enums) > 0 { |
| anyOf = append(anyOf, b.kv("enum", ast.NewList(enums...))) |
| } |
| |
| if nullable { |
| b.setSingle("nullable", ast.NewBool(true), true) |
| } |
| |
| schemas := make([]*ast.StructLit, len(disjuncts)) |
| for i, v := range disjuncts { |
| c := newOASBuilder(b) |
| c.value(v, f) |
| t := c.finish() |
| schemas[i] = (*ast.StructLit)(t) |
| if len(t.Elts) == 0 { |
| if c.typ == "" { |
| return |
| } |
| } |
| } |
| |
| for i, v := range disjuncts { |
| // In OpenAPI schema are open by default. To ensure forward compatibility, |
| // we do not represent closed structs with additionalProperties: false |
| // (this is discouraged and often disallowed by implementions), but |
| // rather enforce this by ensuring uniqueness of the disjuncts. |
| // |
| // TODO: subsumption may currently give false negatives. We are extra |
| // conservative in these instances. |
| subsumed := []ast.Expr{} |
| for j, w := range disjuncts { |
| if i == j { |
| continue |
| } |
| err := v.Subsume(w, cue.Schema()) |
| if err == nil || errors.Is(err, internal.ErrInexact) { |
| subsumed = append(subsumed, schemas[j]) |
| } |
| } |
| |
| t := schemas[i] |
| if len(subsumed) > 0 { |
| // TODO: elide anyOf if there is only one element. This should be |
| // rare if originating from oneOf. |
| exclude := ast.NewStruct("not", |
| ast.NewStruct("anyOf", ast.NewList(subsumed...))) |
| if len(t.Elts) == 0 { |
| t = exclude |
| } else { |
| t = ast.NewStruct("allOf", ast.NewList(t, exclude)) |
| } |
| } |
| anyOf = append(anyOf, t) |
| } |
| |
| b.set("oneOf", ast.NewList(anyOf...)) |
| } |
| |
| func (b *builder) setValueType(v cue.Value) { |
| if b.core != nil { |
| return |
| } |
| |
| k := v.IncompleteKind() &^ adt.NullKind |
| switch k { |
| case cue.BoolKind: |
| b.typ = "boolean" |
| case cue.FloatKind, cue.NumberKind: |
| b.typ = "number" |
| case cue.IntKind: |
| b.typ = "integer" |
| case cue.BytesKind: |
| b.typ = "string" |
| case cue.StringKind: |
| b.typ = "string" |
| case cue.StructKind: |
| b.typ = "object" |
| case cue.ListKind: |
| b.typ = "array" |
| } |
| } |
| |
| func (b *builder) dispatch(f typeFunc, v cue.Value) { |
| if f != nil { |
| f(b, v) |
| return |
| } |
| |
| switch v.IncompleteKind() { |
| case cue.NullKind: |
| // TODO: for JSON schema we would set the type here. For OpenAPI, |
| // it must be nullable. |
| b.setSingle("nullable", ast.NewBool(true), true) |
| |
| case cue.BoolKind: |
| b.setType("boolean", "") |
| // No need to call. |
| |
| case cue.FloatKind, cue.NumberKind: |
| // TODO: |
| // Common Name type format Comments |
| // float number float |
| // double number double |
| b.setType("number", "") // may be overridden to integer |
| b.number(v) |
| |
| case cue.IntKind: |
| // integer integer int32 signed 32 bits |
| // long integer int64 signed 64 bits |
| b.setType("integer", "") // may be overridden to integer |
| b.number(v) |
| |
| // TODO: for JSON schema, consider adding multipleOf: 1. |
| |
| case cue.BytesKind: |
| // byte string byte base64 encoded characters |
| // binary string binary any sequence of octets |
| b.setType("string", "byte") |
| b.bytes(v) |
| case cue.StringKind: |
| // date string date As defined by full-date - RFC3339 |
| // dateTime string date-time As defined by date-time - RFC3339 |
| // password string password A hint to UIs to obscure input |
| b.setType("string", "") |
| b.string(v) |
| case cue.StructKind: |
| b.setType("object", "") |
| b.object(v) |
| case cue.ListKind: |
| b.setType("array", "") |
| b.array(v) |
| } |
| } |
| |
| // object supports the following |
| // - maxProperties: maximum allowed fields in this struct. |
| // - minProperties: minimum required fields in this struct. |
| // - patternProperties: [regexp]: schema |
| // TODO: we can support this once .kv(key, value) allow |
| // foo [=~"pattern"]: type |
| // An instance field must match all schemas for which a regexp matches. |
| // Even though it is not supported in OpenAPI, we should still accept it |
| // when receiving from OpenAPI. We could possibly use disjunctions to encode |
| // this. |
| // - dependencies: what? |
| // - propertyNames: schema |
| // every property name in the enclosed schema matches that of |
| func (b *builder) object(v cue.Value) { |
| // TODO: discriminator objects: we could theoretically derive discriminator |
| // objects automatically: for every object in a oneOf/allOf/anyOf, or any |
| // object composed of the same type, if a property is required and set to a |
| // constant value for each type, it is a discriminator. |
| |
| switch op, a := v.Expr(); op { |
| case cue.CallOp: |
| name := fmt.Sprint(a[0]) |
| switch name { |
| case "struct.MinFields": |
| b.checkArgs(a, 1) |
| b.setFilter("Schema", "minProperties", b.int(a[1])) |
| return |
| |
| case "struct.MaxFields": |
| b.checkArgs(a, 1) |
| b.setFilter("Schema", "maxProperties", b.int(a[1])) |
| return |
| |
| default: |
| b.unsupported(a[0]) |
| return |
| } |
| |
| case cue.NoOp: |
| // TODO: extract format from specific type. |
| |
| default: |
| b.failf(v, "unsupported op %v for object type (%v)", op, v) |
| return |
| } |
| |
| required := []ast.Expr{} |
| for i, _ := v.Fields(); i.Next(); { |
| required = append(required, ast.NewString(i.Label())) |
| } |
| if len(required) > 0 { |
| b.setFilter("Schema", "required", ast.NewList(required...)) |
| } |
| |
| var properties *OrderedMap |
| if b.singleFields != nil { |
| properties = b.singleFields.getMap("properties") |
| } |
| hasProps := properties != nil |
| if !hasProps { |
| properties = &OrderedMap{} |
| } |
| |
| for i, _ := v.Fields(cue.Optional(true), cue.Definitions(true)); i.Next(); { |
| label := i.Label() |
| if b.ctx.isInternal(label) { |
| continue |
| } |
| if i.IsDefinition() && strings.HasPrefix(label, "#") { |
| label = label[1:] |
| } |
| var core *builder |
| if b.core != nil { |
| core = b.core.properties[label] |
| } |
| schema := b.schema(core, label, i.Value()) |
| switch { |
| case i.IsDefinition(): |
| ref := b.ctx.makeRef(b.ctx.instExt, append(b.ctx.path, label)) |
| if ref == "" { |
| continue |
| } |
| b.ctx.schemas.Set(ref, schema) |
| case !b.isNonCore() || len(schema.Elts) > 0: |
| properties.Set(label, schema) |
| } |
| } |
| |
| if !hasProps && properties.len() > 0 { |
| b.setSingle("properties", (*ast.StructLit)(properties), false) |
| } |
| |
| if t, ok := v.Elem(); ok && (b.core == nil || b.core.items == nil) { |
| schema := b.schema(nil, "*", t) |
| if len(schema.Elts) > 0 { |
| b.setSingle("additionalProperties", schema, true) // Not allowed in structural. |
| } |
| } |
| |
| // TODO: maxProperties, minProperties: can be done once we allow cap to |
| // unify with structs. |
| } |
| |
| // List constraints: |
| // |
| // Max and min items. |
| // - maxItems: int (inclusive) |
| // - minItems: int (inclusive) |
| // - items (item type) |
| // schema: applies to all items |
| // array of schemas: |
| // schema at pos must match if both value and items are defined. |
| // - additional items: |
| // schema: where items must be an array of schemas, intstance elements |
| // succeed for if they match this value for any value at a position |
| // greater than that covered by items. |
| // - uniqueItems: bool |
| // TODO: support with list.Unique() unique() or comprehensions. |
| // For the latter, we need equality for all values, which is doable, |
| // but not done yet. |
| // |
| // NOT SUPPORTED IN OpenAPI: |
| // - contains: |
| // schema: an array instance is valid if at least one element matches |
| // this schema. |
| func (b *builder) array(v cue.Value) { |
| |
| switch op, a := v.Expr(); op { |
| case cue.CallOp: |
| name := fmt.Sprint(a[0]) |
| switch name { |
| case "list.UniqueItems", "list.UniqueItems()": |
| b.checkArgs(a, 0) |
| b.setFilter("Schema", "uniqueItems", ast.NewBool(true)) |
| return |
| |
| case "list.MinItems": |
| b.checkArgs(a, 1) |
| b.setFilter("Schema", "minItems", b.int(a[1])) |
| return |
| |
| case "list.MaxItems": |
| b.checkArgs(a, 1) |
| b.setFilter("Schema", "maxItems", b.int(a[1])) |
| return |
| |
| default: |
| b.unsupported(a[0]) |
| return |
| } |
| |
| case cue.NoOp: |
| // TODO: extract format from specific type. |
| |
| default: |
| b.failf(v, "unsupported op %v for array type", op) |
| return |
| } |
| |
| // Possible conjuncts: |
| // - one list (CUE guarantees merging all conjuncts) |
| // - no cap: is unified with list |
| // - unique items: at most one, but idempotent if multiple. |
| // There is never a need for allOf or anyOf. Note that a CUE list |
| // corresponds almost one-to-one to OpenAPI lists. |
| items := []ast.Expr{} |
| count := 0 |
| for i, _ := v.List(); i.Next(); count++ { |
| items = append(items, b.schema(nil, strconv.Itoa(count), i.Value())) |
| } |
| if len(items) > 0 { |
| // TODO: per-item schema are not allowed in OpenAPI, only in JSON Schema. |
| // Perhaps we should turn this into an OR after first normalizing |
| // the entries. |
| b.set("items", ast.NewList(items...)) |
| // panic("per-item types not supported in OpenAPI") |
| } |
| |
| // TODO: |
| // A CUE cap can be a set of discontinuous ranges. If we encounter this, |
| // we can create an allOf(list type, anyOf(ranges)). |
| cap := v.Len() |
| hasMax := false |
| maxLength := int64(math.MaxInt64) |
| |
| if n, capErr := cap.Int64(); capErr == nil { |
| maxLength = n |
| hasMax = true |
| } else { |
| b.value(cap, (*builder).listCap) |
| } |
| |
| if !hasMax || int64(len(items)) < maxLength { |
| if typ, ok := v.Elem(); ok { |
| var core *builder |
| if b.core != nil { |
| core = b.core.items |
| } |
| t := b.schema(core, "*", typ) |
| if len(items) > 0 { |
| b.setFilter("Schema", "additionalItems", t) // Not allowed in structural. |
| } else if !b.isNonCore() || len(t.Elts) > 0 { |
| b.setSingle("items", t, true) |
| } |
| } |
| } |
| } |
| |
| func (b *builder) listCap(v cue.Value) { |
| switch op, a := v.Expr(); op { |
| case cue.LessThanOp: |
| b.setFilter("Schema", "maxItems", b.inta(a[0], -1)) |
| case cue.LessThanEqualOp: |
| b.setFilter("Schema", "maxItems", b.inta(a[0], 0)) |
| case cue.GreaterThanOp: |
| b.setFilter("Schema", "minItems", b.inta(a[0], 1)) |
| case cue.GreaterThanEqualOp: |
| if b.int64(a[0]) > 0 { |
| b.setFilter("Schema", "minItems", b.inta(a[0], 0)) |
| } |
| case cue.NoOp: |
| // must be type, so okay. |
| case cue.NotEqualOp: |
| i := b.int(a[0]) |
| b.setNot("allOff", ast.NewList( |
| b.kv("minItems", i), |
| b.kv("maxItems", i), |
| )) |
| |
| default: |
| b.failf(v, "unsupported op for list capacity %v", op) |
| return |
| } |
| } |
| |
| func (b *builder) number(v cue.Value) { |
| // Multiple conjuncts mostly means just additive constraints. |
| // Type may be number of float. |
| |
| switch op, a := v.Expr(); op { |
| case cue.LessThanOp: |
| if b.ctx.exclusiveBool { |
| b.setFilter("Schema", "exclusiveMaximum", ast.NewBool(true)) |
| b.setFilter("Schema", "maximum", b.big(a[0])) |
| } else { |
| b.setFilter("Schema", "exclusiveMaximum", b.big(a[0])) |
| } |
| |
| case cue.LessThanEqualOp: |
| b.setFilter("Schema", "maximum", b.big(a[0])) |
| |
| case cue.GreaterThanOp: |
| if b.ctx.exclusiveBool { |
| b.setFilter("Schema", "exclusiveMinimum", ast.NewBool(true)) |
| b.setFilter("Schema", "minimum", b.big(a[0])) |
| } else { |
| b.setFilter("Schema", "exclusiveMinimum", b.big(a[0])) |
| } |
| |
| case cue.GreaterThanEqualOp: |
| b.setFilter("Schema", "minimum", b.big(a[0])) |
| |
| case cue.NotEqualOp: |
| i := b.big(a[0]) |
| b.setNot("allOff", ast.NewList( |
| b.kv("minimum", i), |
| b.kv("maximum", i), |
| )) |
| |
| case cue.CallOp: |
| name := fmt.Sprint(a[0]) |
| switch name { |
| case "math.MultipleOf": |
| b.checkArgs(a, 1) |
| b.setFilter("Schema", "multipleOf", b.int(a[1])) |
| |
| default: |
| b.unsupported(a[0]) |
| return |
| } |
| |
| case cue.NoOp: |
| // TODO: extract format from specific type. |
| |
| default: |
| b.failf(v, "unsupported op for number %v", op) |
| } |
| } |
| |
| // Multiple Regexp conjuncts are represented as allOf all other |
| // constraints can be combined unless in the even of discontinuous |
| // lengths. |
| |
| // string supports the following options: |
| // |
| // - maxLength (Unicode codepoints) |
| // - minLength (Unicode codepoints) |
| // - pattern (a regexp) |
| // |
| // The regexp pattern is as follows, and is limited to be a strict subset of RE2: |
| // Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-01#section-3.3 |
| // |
| // JSON schema requires ECMA 262 regular expressions, but |
| // limited to the following constructs: |
| // - simple character classes: [abc] |
| // - range character classes: [a-z] |
| // - complement character classes: [^abc], [^a-z] |
| // - simple quantifiers: +, *, ?, and lazy versions +? *? ?? |
| // - range quantifiers: {x}, {x,y}, {x,}, {x}?, {x,y}?, {x,}? |
| // - begin and end anchors: ^ and $ |
| // - simple grouping: (...) |
| // - alteration: | |
| // This is a subset of RE2 used by CUE. |
| // |
| // Most notably absent: |
| // - the '.' for any character (not sure if that is a doc bug) |
| // - character classes \d \D [[::]] \pN \p{Name} \PN \P{Name} |
| // - word boundaries |
| // - capturing directives. |
| // - flag setting |
| // - comments |
| // |
| // The capturing directives and comments can be removed without |
| // compromising the meaning of the regexp (TODO). Removing |
| // flag setting will be tricky. Unicode character classes, |
| // boundaries, etc can be compiled into simple character classes, |
| // although the resulting regexp will look cumbersome. |
| // |
| func (b *builder) string(v cue.Value) { |
| switch op, a := v.Expr(); op { |
| |
| case cue.RegexMatchOp, cue.NotRegexMatchOp: |
| s, err := a[0].String() |
| if err != nil { |
| // TODO: this may be an unresolved interpolation or expression. Consider |
| // whether it is reasonable to treat unevaluated operands as wholes and |
| // generate a compound regular expression. |
| b.failf(v, "regexp value must be a string: %v", err) |
| return |
| } |
| if op == cue.RegexMatchOp { |
| b.setFilter("Schema", "pattern", ast.NewString(s)) |
| } else { |
| b.setNot("pattern", ast.NewString(s)) |
| } |
| |
| case cue.NoOp, cue.SelectorOp: |
| |
| case cue.CallOp: |
| name := fmt.Sprint(a[0]) |
| switch name { |
| case "strings.MinRunes": |
| b.checkArgs(a, 1) |
| b.setFilter("Schema", "minLength", b.int(a[1])) |
| return |
| |
| case "strings.MaxRunes": |
| b.checkArgs(a, 1) |
| b.setFilter("Schema", "maxLength", b.int(a[1])) |
| return |
| |
| default: |
| b.unsupported(a[0]) |
| return |
| } |
| |
| default: |
| b.failf(v, "unsupported op %v for string type", op) |
| } |
| } |
| |
| func (b *builder) bytes(v cue.Value) { |
| switch op, a := v.Expr(); op { |
| |
| case cue.RegexMatchOp, cue.NotRegexMatchOp: |
| s, err := a[0].Bytes() |
| if err != nil { |
| // TODO: this may be an unresolved interpolation or expression. Consider |
| // whether it is reasonable to treat unevaluated operands as wholes and |
| // generate a compound regular expression. |
| b.failf(v, "regexp value must be of type bytes: %v", err) |
| return |
| } |
| |
| e := ast.NewString(string(s)) |
| if op == cue.RegexMatchOp { |
| b.setFilter("Schema", "pattern", e) |
| } else { |
| b.setNot("pattern", e) |
| } |
| |
| // TODO: support the following JSON schema constraints |
| // - maxLength |
| // - minLength |
| |
| case cue.NoOp, cue.SelectorOp: |
| |
| default: |
| b.failf(v, "unsupported op %v for bytes type", op) |
| } |
| } |
| |
| type builder struct { |
| ctx *buildContext |
| typ string |
| format string |
| singleFields *oaSchema |
| current *oaSchema |
| allOf []*ast.StructLit |
| deprecated bool |
| |
| // Building structural schema |
| core *builder |
| kind cue.Kind |
| filled *ast.StructLit |
| values []cue.Value // in structural mode, all values of not and *Of. |
| keys []string |
| properties map[string]*builder |
| items *builder |
| } |
| |
| func newRootBuilder(c *buildContext) *builder { |
| return &builder{ctx: c} |
| } |
| |
| func newOASBuilder(parent *builder) *builder { |
| core := parent |
| if parent.core != nil { |
| core = parent.core |
| } |
| b := &builder{ |
| core: core, |
| ctx: parent.ctx, |
| typ: parent.typ, |
| format: parent.format, |
| } |
| return b |
| } |
| |
| func (b *builder) isNonCore() bool { |
| return b.core != nil |
| } |
| |
| func (b *builder) setType(t, format string) { |
| if b.typ == "" { |
| b.typ = t |
| if format != "" { |
| b.format = format |
| } |
| } |
| } |
| |
| func setType(t *oaSchema, b *builder) { |
| if b.typ != "" { |
| if b.core == nil || (b.core.typ != b.typ && !b.ctx.structural) { |
| if !t.exists("type") { |
| t.Set("type", ast.NewString(b.typ)) |
| } |
| } |
| } |
| if b.format != "" { |
| if b.core == nil || b.core.format != b.format { |
| t.Set("format", ast.NewString(b.format)) |
| } |
| } |
| } |
| |
| // setFilter is like set, but allows the key-value pair to be filtered. |
| func (b *builder) setFilter(schema, key string, v ast.Expr) { |
| if re := b.ctx.fieldFilter; re != nil && re.MatchString(path.Join(schema, key)) { |
| return |
| } |
| b.set(key, v) |
| } |
| |
| // setSingle sets a value of which there should only be one. |
| func (b *builder) setSingle(key string, v ast.Expr, drop bool) { |
| if b.singleFields == nil { |
| b.singleFields = &OrderedMap{} |
| } |
| if b.singleFields.exists(key) { |
| if !drop { |
| b.failf(cue.Value{}, "more than one value added for key %q", key) |
| } |
| } |
| b.singleFields.Set(key, v) |
| } |
| |
| func (b *builder) set(key string, v ast.Expr) { |
| if b.current == nil { |
| b.current = &OrderedMap{} |
| b.allOf = append(b.allOf, (*ast.StructLit)(b.current)) |
| } else if b.current.exists(key) { |
| b.current = &OrderedMap{} |
| b.allOf = append(b.allOf, (*ast.StructLit)(b.current)) |
| } |
| b.current.Set(key, v) |
| } |
| |
| func (b *builder) kv(key string, value ast.Expr) *ast.StructLit { |
| return ast.NewStruct(key, value) |
| } |
| |
| func (b *builder) setNot(key string, value ast.Expr) { |
| b.add(ast.NewStruct("not", b.kv(key, value))) |
| } |
| |
| func (b *builder) finish() *ast.StructLit { |
| var t *OrderedMap |
| |
| if b.filled != nil { |
| return b.filled |
| } |
| switch len(b.allOf) { |
| case 0: |
| t = &OrderedMap{} |
| |
| case 1: |
| hasRef := false |
| for _, e := range b.allOf[0].Elts { |
| if f, ok := e.(*ast.Field); ok { |
| name, _, _ := ast.LabelName(f.Label) |
| hasRef = hasRef || name == "$ref" |
| } |
| } |
| if !hasRef || b.singleFields == nil { |
| t = (*OrderedMap)(b.allOf[0]) |
| break |
| } |
| fallthrough |
| |
| default: |
| exprs := []ast.Expr{} |
| if t != nil { |
| exprs = append(exprs, (*ast.StructLit)(t)) |
| } |
| for _, s := range b.allOf { |
| exprs = append(exprs, s) |
| } |
| t = &OrderedMap{} |
| t.Set("allOf", ast.NewList(exprs...)) |
| } |
| if b.singleFields != nil { |
| b.singleFields.Elts = append(b.singleFields.Elts, t.Elts...) |
| t = b.singleFields |
| } |
| if b.deprecated { |
| t.Set("deprecated", ast.NewBool(true)) |
| } |
| setType(t, b) |
| sortSchema((*ast.StructLit)(t)) |
| return (*ast.StructLit)(t) |
| } |
| |
| func (b *builder) add(t *ast.StructLit) { |
| b.allOf = append(b.allOf, t) |
| } |
| |
| func (b *builder) addConjunct(f func(*builder)) { |
| c := newOASBuilder(b) |
| f(c) |
| b.add((*ast.StructLit)(c.finish())) |
| } |
| |
| func (b *builder) addRef(v cue.Value, inst *cue.Instance, ref []string) { |
| name := b.ctx.makeRef(inst, ref) |
| b.addConjunct(func(b *builder) { |
| b.allOf = append(b.allOf, ast.NewStruct( |
| "$ref", ast.NewString(path.Join("#", b.ctx.refPrefix, name)))) |
| }) |
| |
| if b.ctx.inst != inst { |
| b.ctx.externalRefs[name] = &externalType{ |
| ref: name, |
| inst: inst, |
| path: ref, |
| value: v, |
| } |
| } |
| } |
| |
| func (b *buildContext) makeRef(inst *cue.Instance, ref []string) string { |
| ref = append([]string{}, ref...) |
| for i, s := range ref { |
| if strings.HasPrefix(s, "#") { |
| ref[i] = s[1:] |
| } |
| } |
| a := make([]string, 0, len(ref)+3) |
| if b.nameFunc != nil { |
| a = append(a, b.nameFunc(inst, ref)) |
| } else { |
| a = append(a, ref...) |
| } |
| return strings.Join(a, ".") |
| } |
| |
| func (b *builder) int64(v cue.Value) int64 { |
| v, _ = v.Default() |
| i, err := v.Int64() |
| if err != nil { |
| b.failf(v, "could not retrieve int: %v", err) |
| } |
| return i |
| } |
| |
| func (b *builder) intExpr(i int64) ast.Expr { |
| return &ast.BasicLit{ |
| Kind: token.INT, |
| Value: fmt.Sprint(i), |
| } |
| } |
| |
| func (b *builder) int(v cue.Value) ast.Expr { |
| return b.intExpr(b.int64(v)) |
| } |
| |
| func (b *builder) inta(v cue.Value, offset int64) ast.Expr { |
| return b.intExpr(b.int64(v) + offset) |
| } |
| |
| func (b *builder) decode(v cue.Value) ast.Expr { |
| v, _ = v.Default() |
| return v.Syntax().(ast.Expr) |
| } |
| |
| func (b *builder) big(v cue.Value) ast.Expr { |
| v, _ = v.Default() |
| return v.Syntax().(ast.Expr) |
| } |