diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index c491a1c57..6a32f5d5d 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -36,6 +36,7 @@ type buildContext struct { path []string expandRefs bool + structural bool nameFunc func(inst *cue.Instance, path []string) string descFunc func(v cue.Value) string fieldFilter *regexp.Regexp @@ -80,6 +81,7 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *OrderedMap, err error) inst: inst, refPrefix: "components/schemas", expandRefs: g.ExpandReferences, + structural: g.ExpandReferences, nameFunc: g.ReferenceFunc, descFunc: g.DescriptionFunc, schemas: &OrderedMap{}, @@ -137,7 +139,7 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *OrderedMap, err error) } func (c *buildContext) build(name string, v cue.Value) *oaSchema { - return newRootBuilder(c).schema(name, v) + return newCoreBuilder(c).schema(nil, name, v) } // isInternal reports whether or not to include this type. @@ -168,37 +170,67 @@ func (b *builder) checkArgs(a []cue.Value, n int) { } } -func (b *builder) schema(name string, v cue.Value) *oaSchema { +func (b *builder) schema(core *builder, name string, v cue.Value) *oaSchema { oldPath := b.ctx.path b.ctx.path = append(b.ctx.path, name) defer func() { b.ctx.path = oldPath }() - c := newRootBuilder(b.ctx) - c.format = extractFormat(v) - isRef := c.value(v, nil) - schema := c.finish() + var c *builder + if core == nil && b.ctx.structural { + c = newCoreBuilder(b.ctx) + c.buildCore(v) // initialize core structure + c.coreSchema(name) // build the + } else { + c = newRootBuilder(b.ctx) + c.core = core + } - if !isRef { - 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()) - } + 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")) - schema.Set("description", str) + } + if len(doc) > 0 { + str := strings.TrimSpace(strings.Join(doc, "\n\n")) + b.setSingle("description", str, true) + } +} + +func (b *builder) fillSchema(v cue.Value) *oaSchema { + if b.filled != nil { + return b.filled + } + + b.setValueType(v) + b.format = extractFormat(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) } } - simplify(c, schema) + schema := b.finish() + + simplify(b, schema) sortSchema(schema) + b.filled = schema return schema } @@ -230,6 +262,8 @@ var fieldOrder = map[string]int{ "minLength": 16, "maxLength": 15, "items": 14, + "enum": 13, + "default": 12, } func (b *builder) resolve(v cue.Value) cue.Value { @@ -302,8 +336,9 @@ func (b *builder) value(v cue.Value, f typeFunc) (isRef bool) { switch { case isConcrete(v): b.dispatch(f, v) - b.set("enum", []interface{}{b.decode(v)}) - + if !b.isNonCore() { + b.set("enum", []interface{}{b.decode(v)}) + } default: if a := appendSplit(nil, cue.OrOp, v); len(a) > 1 { b.disjunction(a, f) @@ -331,7 +366,9 @@ func (b *builder) value(v cue.Value, f typeFunc) (isRef bool) { } fallthrough default: - b.setFilter("Schema", "default", v) + if !b.isNonCore() { + b.setFilter("Schema", "default", v) + } } } return isRef @@ -418,34 +455,64 @@ func (b *builder) disjunction(a []cue.Value, f typeFunc) { if len(disjuncts) == 1 { b.value(disjuncts[0], f) } - if len(enums) > 0 { + if len(enums) > 0 && !b.isNonCore() { b.set("enum", enums) } if nullable { - b.set("nullable", true) + b.setSingle("nullable", true, true) // allowed in Structural } return } - b.addConjunct(func(b *builder) { - anyOf := []*oaSchema{} - if len(enums) > 0 { - anyOf = append(anyOf, b.kv("enum", enums)) - } + anyOf := []*oaSchema{} + if len(enums) > 0 { + anyOf = append(anyOf, b.kv("enum", enums)) + } - for _, v := range disjuncts { - c := newOASBuilder(b) - c.value(v, f) - anyOf = append(anyOf, c.finish()) + hasEmpty := false + for _, v := range disjuncts { + c := newOASBuilder(b) + c.value(v, f) + t := c.finish() + if len(t.kvs) == 0 { + hasEmpty = true } + anyOf = append(anyOf, t) + } - // TODO: analyze CUE structs to figure out if it should be oneOf or - // anyOf. As the source is protobuf for now, it is always oneOf. + // If any of the types was "any", a oneOf may be discarded. + if !hasEmpty { b.set("oneOf", anyOf) - if nullable { - b.set("nullable", true) - } - }) + } + + // TODO: analyze CUE structs to figure out if it should be oneOf or + // anyOf. As the source is protobuf for now, it is always oneOf. + if nullable { + b.setSingle("nullable", true, true) + } +} + +func (b *builder) setValueType(v cue.Value) { + if b.core != nil { + return + } + + switch v.IncompleteKind() &^ cue.BottomKind { + 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) { @@ -458,7 +525,7 @@ func (b *builder) dispatch(f typeFunc, v cue.Value) { case cue.NullKind: // TODO: for JSON schema we would set the type here. For OpenAPI, // it must be nullable. - b.set("nullable", true) + b.setSingle("nullable", true, true) case cue.BoolKind: b.setType("boolean", "") @@ -554,16 +621,36 @@ func (b *builder) object(v cue.Value) { b.setFilter("Schema", "required", required) } - properties := &OrderedMap{} + 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.Hidden(false)); i.Next(); { - properties.Set(i.Label(), b.schema(i.Label(), i.Value())) + label := i.Label() + var core *builder + if b.core != nil { + core = b.core.properties[label] + } + schema := b.schema(core, label, i.Value()) + if !b.isNonCore() || len(schema.kvs) > 0 { + properties.Set(label, schema) + } } - if len(properties.kvs) > 0 { - b.set("properties", properties) + + if !hasProps && len(properties.kvs) > 0 { + b.setSingle("properties", properties, false) } - if t, ok := v.Elem(); ok { - b.setFilter("Schema", "additionalProperties", b.schema("*", t)) + if t, ok := v.Elem(); ok && (b.core == nil || b.core.items == nil) { + schema := b.schema(nil, "*", t) + if len(schema.kvs) > 0 { + b.setSingle("additionalProperties", schema, true) // Not allowed in structural. + } } // TODO: maxProperties, minProperties: can be done once we allow cap to @@ -635,7 +722,7 @@ func (b *builder) array(v cue.Value) { items := []*oaSchema{} count := 0 for i, _ := v.List(); i.Next(); count++ { - items = append(items, b.schema(strconv.Itoa(count), i.Value())) + 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. @@ -661,11 +748,15 @@ func (b *builder) array(v cue.Value) { if !hasMax || int64(len(items)) < maxLength { if typ, ok := v.Elem(); ok { - t := b.schema("*", typ) + 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) - } else { - b.set("items", t) + b.setFilter("Schema", "additionalItems", t) // Not allowed in structural. + } else if !b.isNonCore() || len(t.kvs) > 0 { + b.setSingle("items", t, true) } } } @@ -855,12 +946,21 @@ func (b *builder) bytes(v cue.Value) { } type builder struct { - ctx *buildContext - typ string - format string - current *oaSchema - allOf []*oaSchema - enums []interface{} + ctx *buildContext + typ string + format string + singleFields *oaSchema + current *oaSchema + allOf []*oaSchema + + // Building structural schema + core *builder + kind cue.Kind + filled *oaSchema + 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 { @@ -868,7 +968,12 @@ func newRootBuilder(c *buildContext) *builder { } 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, @@ -876,6 +981,10 @@ func newOASBuilder(parent *builder) *builder { return b } +func (b *builder) isNonCore() bool { + return b.core != nil +} + func (b *builder) setType(t, format string) { if b.typ == "" { b.typ = t @@ -887,8 +996,14 @@ func (b *builder) setType(t, format string) { func setType(t *oaSchema, b *builder) { if b.typ != "" { - t.Set("type", b.typ) - if b.format != "" { + if b.core == nil || (b.core.typ != b.typ && !b.ctx.structural) { + if !t.exists("type") { + t.Set("type", b.typ) + } + } + } + if b.format != "" { + if b.core == nil || b.core.format != b.format { t.Set("format", b.format) } } @@ -902,11 +1017,23 @@ func (b *builder) setFilter(schema, key string, v interface{}) { b.set(key, v) } +// setSingle sets a value of which there should only be one. +func (b *builder) setSingle(key string, v interface{}, 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 interface{}) { if b.current == nil { b.current = &OrderedMap{} b.allOf = append(b.allOf, b.current) - setType(b.current, b) } else if b.current.exists(key) { b.current = &OrderedMap{} b.allOf = append(b.allOf, b.current) @@ -916,7 +1043,6 @@ func (b *builder) set(key string, v interface{}) { func (b *builder) kv(key string, value interface{}) *oaSchema { constraint := &OrderedMap{} - setType(constraint, b) constraint.Set(key, value) return constraint } @@ -927,24 +1053,28 @@ func (b *builder) setNot(key string, value interface{}) { b.add(not) } -func (b *builder) finish() *oaSchema { +func (b *builder) finish() (t *oaSchema) { + if b.filled != nil { + return b.filled + } switch len(b.allOf) { case 0: - t := &OrderedMap{} - if b.typ != "" { - setType(t, b) - } - return t + t = &OrderedMap{} case 1: - setType(b.allOf[0], b) - return b.allOf[0] + t = b.allOf[0] default: - t := &OrderedMap{} + t = &OrderedMap{} t.Set("allOf", b.allOf) - return t } + if b.singleFields != nil { + b.singleFields.kvs = append(b.singleFields.kvs, t.kvs...) + t = b.singleFields + } + setType(t, b) + sortSchema(t) + return t } func (b *builder) add(t *oaSchema) { diff --git a/encoding/openapi/crd.go b/encoding/openapi/crd.go new file mode 100644 index 000000000..28b935f27 --- /dev/null +++ b/encoding/openapi/crd.go @@ -0,0 +1,167 @@ +// 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 + +// This file contains functionality for structural schema, a subset of OpenAPI +// used for CRDs. +// +// See https://kubernetes.io/blog/2019/06/20/crd-structural-schema/ for details. +// +// Insofar definitions are compatible, openapi normalizes to structural whenever +// possible. +// +// A core structural schema is only made out of the following fields: +// +// - properties +// - items +// - additionalProperties +// - type +// - nullable +// - title +// - descriptions. +// +// Where the types must be defined for all fields. +// +// In addition, the value validations constraints may be used as defined in +// OpenAPI, with the restriction that +// - within the logical constraints anyOf, allOf, oneOf, and not +// additionalProperties, type, nullable, title, and description may not be used. +// - all mentioned fields must be defined in the core schema. +// +// It appears that CRDs do not allow references. +// + +import ( + "cuelang.org/go/cue" +) + +// newCoreBuilder returns a builder that represents a structural schema. +func newCoreBuilder(c *buildContext) *builder { + b := newRootBuilder(c) + b.properties = map[string]*builder{} + return b +} + +// coreSchema creates the core part of a structural OpenAPI. +func (b *builder) coreSchema(name string) *oaSchema { + oldPath := b.ctx.path + b.ctx.path = append(b.ctx.path, name) + defer func() { b.ctx.path = oldPath }() + + switch b.kind { + case cue.ListKind: + if b.items != nil { + b.setType("array", "") + schema := b.items.coreSchema("*") + b.setSingle("items", schema, false) + } + + case cue.StructKind: + p := &OrderedMap{} + for _, k := range b.keys { + sub := b.properties[k] + p.Set(k, sub.coreSchema(k)) + } + if len(p.kvs) > 0 || b.items != nil { + b.setType("object", "") + } + if len(p.kvs) > 0 { + b.setSingle("properties", p, false) + } + // TODO: in Structural schema only one of these is allowed. + if b.items != nil { + schema := b.items.coreSchema("*") + b.setSingle("additionalProperties", schema, false) + } + } + + // If there was only a single value associated with this node, we can + // safely assume there were no disjunctions etc. In structural mode this + // is the only chance we get to set certain properties. + if len(b.values) == 1 { + return b.fillSchema(b.values[0]) + } + + // TODO: do type analysis if we have multiple values and piece out more + // information that applies to all possible instances. + + return b.finish() +} + +// buildCore collects the CUE values for the structural OpenAPI tree. +// To this extent, all fields of both conjunctions and disjunctions are +// collected in a single properties map. +func (b *builder) buildCore(v cue.Value) { + if !b.ctx.expandRefs { + _, r := v.Reference() + if len(r) > 0 { + return + } + } + b.getDoc(v) + format := extractFormat(v) + if format != "" { + b.format = format + } else { + v = v.Eval() + b.kind = v.IncompleteKind() &^ cue.BottomKind + + switch b.kind { + case cue.StructKind: + if typ, ok := v.Elem(); ok { + if b.items == nil { + b.items = newCoreBuilder(b.ctx) + } + b.items.buildCore(typ) + } + b.buildCoreStruct(v) + + case cue.ListKind: + if typ, ok := v.Elem(); ok { + if b.items == nil { + b.items = newCoreBuilder(b.ctx) + } + b.items.buildCore(typ) + } + } + } + + for _, bv := range b.values { + if bv.Equals(v) { + return + } + } + b.values = append(b.values, v) +} + +func (b *builder) buildCoreStruct(v cue.Value) { + op, args := v.Expr() + switch op { + case cue.OrOp, cue.AndOp: + for _, v := range args { + b.buildCore(v) + } + } + for i, _ := v.Fields(cue.Optional(true), cue.Hidden(false)); i.Next(); { + label := i.Label() + sub, ok := b.properties[label] + if !ok { + sub = newCoreBuilder(b.ctx) + b.properties[label] = sub + b.keys = append(b.keys, label) + } + sub.buildCore(i.Value()) + } +} diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go index 1459527d2..ea634ace3 100644 --- a/encoding/openapi/openapi_test.go +++ b/encoding/openapi/openapi_test.go @@ -39,6 +39,10 @@ func TestParseDefinitions(t *testing.T) { in, out string config *Config }{{ + "structural.cue", + "structural.json", + resolveRefs, + }, { "simple.cue", "simple.json", resolveRefs, @@ -138,3 +142,27 @@ func TestParseDefinitions(t *testing.T) { }) } } + +// This is for debugging purposes. Do not remove. +func TestX(t *testing.T) { + t.Skip() + + var r cue.Runtime + inst, err := r.Compile("test", ` + AnyField: "any value" + `) + if err != nil { + t.Fatal(err) + } + + b, err := Gen(inst, &Config{ + ExpandReferences: true, + }) + if err != nil { + t.Fatal(err) + } + + var out = &bytes.Buffer{} + _ = json.Indent(out, b, "", " ") + t.Error(out.String()) +} diff --git a/encoding/openapi/testdata/array.json b/encoding/openapi/testdata/array.json index 570d65918..5a9136a23 100644 --- a/encoding/openapi/testdata/array.json +++ b/encoding/openapi/testdata/array.json @@ -9,12 +9,13 @@ "bar": { "type": "array", "items": { - "default": "1", + "type": "string", "enum": [ "1", "2", "3" - ] + ], + "default": "1" } }, "foo": { @@ -28,12 +29,13 @@ "e": { "type": "array", "items": { - "default": "1", + "type": "string", "enum": [ "1", "2", "3" - ] + ], + "default": "1" } } } @@ -52,12 +54,13 @@ }, "MyEnum": { "description": "MyEnum", - "default": "1", + "type": "string", "enum": [ "1", "2", "3" - ] + ], + "default": "1" }, "MyStruct": { "description": "MyStruct", @@ -69,12 +72,13 @@ "e": { "type": "array", "items": { - "default": "1", + "type": "string", "enum": [ "1", "2", "3" - ] + ], + "default": "1" } } } diff --git a/encoding/openapi/testdata/nums.json b/encoding/openapi/testdata/nums.json index fb6efba2e..69874fe76 100644 --- a/encoding/openapi/testdata/nums.json +++ b/encoding/openapi/testdata/nums.json @@ -10,14 +10,11 @@ "neq": { "type": "number", "not": { - "type": "number", "allOff": [ { - "type": "number", "minimum": 4 }, { - "type": "number", "maximum": 4 } ] diff --git a/encoding/openapi/testdata/oneof-funcs.json b/encoding/openapi/testdata/oneof-funcs.json index dd0755e77..157b53c38 100644 --- a/encoding/openapi/testdata/oneof-funcs.json +++ b/encoding/openapi/testdata/oneof-funcs.json @@ -8,9 +8,9 @@ "schemas": { "MYSTRING": { "description": "Randomly picked description from a set of size one.", + "type": "object", "oneOf": [ { - "type": "object", "required": [ "exact" ], @@ -23,7 +23,6 @@ } }, { - "type": "object", "required": [ "regex" ], diff --git a/encoding/openapi/testdata/oneof-resolve.json b/encoding/openapi/testdata/oneof-resolve.json index 217dc9f4b..629e0dd14 100644 --- a/encoding/openapi/testdata/oneof-resolve.json +++ b/encoding/openapi/testdata/oneof-resolve.json @@ -7,30 +7,27 @@ "components": { "schemas": { "MyString": { + "type": "object", + "properties": { + "exact": { + "type": "string", + "format": "string" + }, + "regex": { + "type": "string", + "format": "string" + } + }, "oneOf": [ { - "type": "object", "required": [ "exact" - ], - "properties": { - "exact": { - "type": "string", - "format": "string" - } - } + ] }, { - "type": "object", "required": [ "regex" - ], - "properties": { - "regex": { - "type": "string", - "format": "string" - } - } + ] } ] }, @@ -46,60 +43,54 @@ ], "properties": { "include": { + "type": "object", + "properties": { + "exact": { + "type": "string", + "format": "string" + }, + "regex": { + "type": "string", + "format": "string" + } + }, "oneOf": [ { - "type": "object", "required": [ "exact" - ], - "properties": { - "exact": { - "type": "string", - "format": "string" - } - } + ] }, { - "type": "object", "required": [ "regex" - ], - "properties": { - "regex": { - "type": "string", - "format": "string" - } - } + ] } ] }, "exclude": { "type": "array", "items": { + "type": "object", + "properties": { + "exact": { + "type": "string", + "format": "string" + }, + "regex": { + "type": "string", + "format": "string" + } + }, "oneOf": [ { - "type": "object", "required": [ "exact" - ], - "properties": { - "exact": { - "type": "string", - "format": "string" - } - } + ] }, { - "type": "object", "required": [ "regex" - ], - "properties": { - "regex": { - "type": "string", - "format": "string" - } - } + ] } ] } diff --git a/encoding/openapi/testdata/oneof.json b/encoding/openapi/testdata/oneof.json index 2781b7b4b..b2c0c9798 100644 --- a/encoding/openapi/testdata/oneof.json +++ b/encoding/openapi/testdata/oneof.json @@ -4,9 +4,9 @@ "components": { "schemas": { "MyString": { + "type": "object", "oneOf": [ { - "type": "object", "required": [ "exact" ], @@ -18,7 +18,6 @@ } }, { - "type": "object", "required": [ "regex" ], diff --git a/encoding/openapi/testdata/openapi-norefs.json b/encoding/openapi/testdata/openapi-norefs.json index 658290eda..0c7e92290 100644 --- a/encoding/openapi/testdata/openapi-norefs.json +++ b/encoding/openapi/testdata/openapi-norefs.json @@ -8,76 +8,63 @@ "schemas": { "MyMessage": { "description": "MyMessage is my message.", - "allOf": [ - { + "type": "object", + "required": [ + "foo", + "bar" + ], + "properties": { + "port": { "type": "object", "required": [ - "foo", - "bar" + "port", + "obj" ], "properties": { "port": { - "type": "object", - "required": [ - "port", - "obj" - ], - "properties": { - "port": { - "type": "integer" - }, - "obj": { - "type": "array", - "items": { - "type": "integer" - } - } - } + "type": "integer" }, - "foo": { - "type": "number", - "exclusiveMinimum": 10, - "exclusiveMaximum": 1000 - }, - "bar": { + "obj": { "type": "array", "items": { - "type": "string", - "format": "string" + "type": "integer" } } } }, + "foo": { + "type": "number", + "exclusiveMinimum": 10, + "exclusiveMaximum": 1000 + }, + "bar": { + "type": "array", + "items": { + "type": "string", + "format": "string" + } + }, + "a": { + "description": "Field a.", + "type": "integer", + "enum": [ + 1 + ] + }, + "b": { + "type": "string", + "format": "string" + } + }, + "oneOf": [ { - "type": "object", - "oneOf": [ - { - "type": "object", - "required": [ - "a" - ], - "properties": { - "a": { - "description": "Field a.", - "type": "integer", - "enum": [ - 1 - ] - } - } - }, - { - "type": "object", - "required": [ - "b" - ], - "properties": { - "b": { - "type": "string", - "format": "string" - } - } - } + "required": [ + "a" + ] + }, + { + "required": [ + "b" ] } ] @@ -105,150 +92,122 @@ "format": "int32" }, "YourMessage": { + "type": "object", + "properties": { + "a": { + "type": "string", + "format": "string" + }, + "b": { + "format": "string" + } + }, "oneOf": [ { - "type": "object", "required": [ "b" - ], - "properties": { - "a": { - "type": "string", - "format": "string" - }, - "b": { - "type": "string", - "format": "string" - } - } + ] }, { - "type": "object", "required": [ "b" - ], - "properties": { - "a": { - "type": "string", - "format": "string" - }, - "b": { - "type": "number" - } - } + ] } ] }, "YourMessage2": { + "type": "object", + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + }, + "e": { + "type": "number" + }, + "f": { + "type": "number" + }, + "d": { + "type": "number" + }, + "b": { + "type": "number" + } + }, "allOf": [ { "oneOf": [ { - "type": "object", "required": [ "a" - ], - "properties": { - "a": { - "type": "number" - } - } + ] }, { - "type": "object", "required": [ "b" - ], - "properties": { - "b": { - "type": "number" - } - } + ] } ] }, { "oneOf": [ { - "type": "object", "required": [ "c" - ], - "properties": { - "c": { - "type": "number" - } - } + ] }, { - "type": "object", "required": [ "d" - ], - "properties": { - "d": { - "type": "number" - } - } + ] } ] }, { "oneOf": [ { - "type": "object", "required": [ "e" - ], - "properties": { - "e": { - "type": "number" - } - } + ] }, { - "type": "object", "required": [ "f" - ], - "properties": { - "f": { - "type": "number" - } - } + ] } ] } ] }, "Msg2": { + "type": "object", + "properties": { + "b": { + "type": "number" + }, + "a": { + "type": "string", + "format": "string" + } + }, "oneOf": [ { - "type": "object", "required": [ "b" - ], - "properties": { - "b": { - "type": "number" - } - } + ] }, { - "type": "object", "required": [ "a" - ], - "properties": { - "a": { - "type": "string", - "format": "string" - } - } + ] } ] }, "Enum": { + "type": "string", "enum": [ "foo", "bar", @@ -267,47 +226,30 @@ ] }, "DefaultStruct": { - "allOf": [ + "type": "object", + "properties": { + "port": {}, + "obj": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "default": { + "port": 1 + }, + "oneOf": [ { - "oneOf": [ - { - "type": "object", - "required": [ - "port", - "obj" - ], - "properties": { - "port": { - "type": "integer" - }, - "obj": { - "type": "array", - "items": { - "type": "integer" - } - } - } - }, - { - "type": "object", - "required": [ - "port" - ], - "properties": { - "port": { - "type": "integer", - "enum": [ - 1 - ] - } - } - } + "required": [ + "port", + "obj" ] }, { - "default": { - "port": 1 - } + "required": [ + "port" + ] } ] } diff --git a/encoding/openapi/testdata/openapi.json b/encoding/openapi/testdata/openapi.json index c60e19ee1..a8b888ae0 100644 --- a/encoding/openapi/testdata/openapi.json +++ b/encoding/openapi/testdata/openapi.json @@ -5,70 +5,61 @@ "schemas": { "MyMessage": { "description": "MyMessage is my message.", - "allOf": [ - { + "type": "object", + "required": [ + "foo", + "bar" + ], + "properties": { + "port": { "type": "object", + "$ref": "#/components/schemas/Port" + }, + "foo": { + "type": "number", + "allOf": [ + { + "$ref": "#/components/schemas/Int32" + }, + { + "exclusiveMinimum": 10, + "exclusiveMaximum": 1000 + } + ] + }, + "bar": { + "type": "array", + "items": { + "type": "string", + "format": "string" + } + } + }, + "oneOf": [ + { "required": [ - "foo", - "bar" + "a" ], "properties": { - "port": { - "type": "object", - "$ref": "#/components/schemas/Port" - }, - "foo": { - "allOf": [ - { - "$ref": "#/components/schemas/Int32" - }, - { - "type": "number", - "exclusiveMinimum": 10, - "exclusiveMaximum": 1000 - } + "a": { + "description": "Field a.", + "type": "integer", + "enum": [ + 1 ] - }, - "bar": { - "type": "array", - "items": { - "type": "string", - "format": "string" - } } } }, { - "type": "object", - "oneOf": [ - { - "type": "object", - "required": [ - "a" - ], - "properties": { - "a": { - "description": "Field a.", - "type": "integer", - "enum": [ - 1 - ] - } - } - }, - { - "type": "object", - "required": [ - "b" - ], - "properties": { - "b": { - "type": "string", - "format": "string" - } - } + "required": [ + "b" + ], + "properties": { + "b": { + "type": "string", + "format": "string" } - ] + } } ] }, @@ -95,9 +86,9 @@ "format": "int32" }, "YourMessage": { + "type": "object", "oneOf": [ { - "type": "object", "required": [ "b" ], @@ -113,7 +104,6 @@ } }, { - "type": "object", "required": [ "b" ], @@ -130,11 +120,11 @@ ] }, "YourMessage2": { + "type": "object", "allOf": [ { "oneOf": [ { - "type": "object", "required": [ "a" ], @@ -145,7 +135,6 @@ } }, { - "type": "object", "required": [ "b" ], @@ -160,7 +149,6 @@ { "oneOf": [ { - "type": "object", "required": [ "c" ], @@ -171,7 +159,6 @@ } }, { - "type": "object", "required": [ "d" ], @@ -186,7 +173,6 @@ { "oneOf": [ { - "type": "object", "required": [ "e" ], @@ -197,7 +183,6 @@ } }, { - "type": "object", "required": [ "f" ], @@ -212,9 +197,9 @@ ] }, "Msg2": { + "type": "object", "oneOf": [ { - "type": "object", "required": [ "b" ], @@ -225,7 +210,6 @@ } }, { - "type": "object", "required": [ "a" ], @@ -239,6 +223,7 @@ ] }, "Enum": { + "type": "string", "enum": [ "foo", "bar", @@ -257,31 +242,25 @@ ] }, "DefaultStruct": { - "allOf": [ + "type": "object", + "default": { + "port": 1 + }, + "oneOf": [ { - "oneOf": [ - { - "$ref": "#/components/schemas/Port" - }, - { - "type": "object", - "required": [ - "port" - ], - "properties": { - "port": { - "type": "integer", - "enum": [ - 1 - ] - } - } - } - ] + "$ref": "#/components/schemas/Port" }, { - "default": { - "port": 1 + "required": [ + "port" + ], + "properties": { + "port": { + "type": "integer", + "enum": [ + 1 + ] + } } } ] diff --git a/encoding/openapi/testdata/strings.json b/encoding/openapi/testdata/strings.json index d62bfff45..4b8123bb0 100644 --- a/encoding/openapi/testdata/strings.json +++ b/encoding/openapi/testdata/strings.json @@ -23,7 +23,6 @@ "myAntiPattern": { "type": "string", "not": { - "type": "string", "pattern": "foo.*bar" } } diff --git a/encoding/openapi/testdata/structural.cue b/encoding/openapi/testdata/structural.cue new file mode 100644 index 000000000..f1bb1c60e --- /dev/null +++ b/encoding/openapi/testdata/structural.cue @@ -0,0 +1,44 @@ +import "time" + +Attributes: { + // A map of attribute name to its value. + attributes: { + <_>: AttrValue + } +} + +// The attribute value. +AttrValue: {} + +AttrValue: { + // Used for values of type STRING, DNS_NAME, EMAIL_ADDRESS, and URI + stringValue: string @protobuf(2,name=string_value) +} | { + // Used for values of type INT64 + int64Value: int64 @protobuf(3,name=int64_value) +} | { + // Used for values of type DOUBLE + doubleValue: float64 @protobuf(4,type=double,name=double_value) +} | { + // Used for values of type BOOL + boolValue: bool @protobuf(5,name=bool_value) +} | { + // Used for values of type BYTES + bytesValue: bytes @protobuf(6,name=bytes_value) +} | { + // Used for values of type TIMESTAMP + timestampValue: time.Time @protobuf(7,type=google.protobuf.Timestamp,name=timestamp_value) +} | { + // Used for values of type DURATION + durationValue: time.Duration @protobuf(8,type=google.protobuf.Duration,name=duration_value) +} | { + // Used for values of type STRING_MAP + stringMapValue: Attributes_StringMap @protobuf(9,type=StringMap,name=string_map_value) +} + +Attributes_StringMap: { + // Holds a set of name/value pairs. + entries: { + <_>: string + } @protobuf(1,type=map) +} diff --git a/encoding/openapi/testdata/structural.json b/encoding/openapi/testdata/structural.json new file mode 100644 index 000000000..b1beee289 --- /dev/null +++ b/encoding/openapi/testdata/structural.json @@ -0,0 +1,234 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "test", + "version": "v1" + }, + "components": { + "schemas": { + "Attributes": { + "type": "object", + "required": [ + "attributes" + ], + "properties": { + "attributes": { + "description": "A map of attribute name to its value.", + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "stringValue": { + "description": "Used for values of type STRING, DNS_NAME, EMAIL_ADDRESS, and URI", + "type": "string", + "format": "string" + }, + "int64Value": { + "description": "Used for values of type INT64", + "type": "integer", + "format": "int64" + }, + "doubleValue": { + "description": "Used for values of type DOUBLE", + "type": "number", + "format": "double" + }, + "boolValue": { + "description": "Used for values of type BOOL", + "type": "boolean" + }, + "bytesValue": { + "description": "Used for values of type BYTES", + "type": "string", + "format": "binary" + }, + "timestampValue": { + "description": "Used for values of type TIMESTAMP", + "type": "string", + "format": "dateTime" + }, + "durationValue": { + "description": "Used for values of type DURATION", + "type": "string" + }, + "stringMapValue": { + "description": "Used for values of type STRING_MAP", + "type": "object", + "required": [ + "entries" + ], + "properties": { + "entries": { + "description": "Holds a set of name/value pairs.", + "type": "object", + "additionalProperties": { + "type": "string", + "format": "string" + } + } + } + } + }, + "oneOf": [ + { + "required": [ + "stringValue" + ] + }, + { + "required": [ + "int64Value" + ] + }, + { + "required": [ + "doubleValue" + ] + }, + { + "required": [ + "boolValue" + ] + }, + { + "required": [ + "bytesValue" + ] + }, + { + "required": [ + "timestampValue" + ] + }, + { + "required": [ + "durationValue" + ] + }, + { + "required": [ + "stringMapValue" + ] + } + ] + } + } + } + }, + "AttrValue": { + "description": "The attribute value.", + "type": "object", + "properties": { + "stringValue": { + "description": "Used for values of type STRING, DNS_NAME, EMAIL_ADDRESS, and URI", + "type": "string", + "format": "string" + }, + "int64Value": { + "description": "Used for values of type INT64", + "type": "integer", + "format": "int64" + }, + "doubleValue": { + "description": "Used for values of type DOUBLE", + "type": "number", + "format": "double" + }, + "boolValue": { + "description": "Used for values of type BOOL", + "type": "boolean" + }, + "bytesValue": { + "description": "Used for values of type BYTES", + "type": "string", + "format": "binary" + }, + "timestampValue": { + "description": "Used for values of type TIMESTAMP", + "type": "string", + "format": "dateTime" + }, + "durationValue": { + "description": "Used for values of type DURATION", + "type": "string" + }, + "stringMapValue": { + "description": "Used for values of type STRING_MAP", + "type": "object", + "required": [ + "entries" + ], + "properties": { + "entries": { + "description": "Holds a set of name/value pairs.", + "type": "object", + "additionalProperties": { + "type": "string", + "format": "string" + } + } + } + } + }, + "oneOf": [ + { + "required": [ + "stringValue" + ] + }, + { + "required": [ + "int64Value" + ] + }, + { + "required": [ + "doubleValue" + ] + }, + { + "required": [ + "boolValue" + ] + }, + { + "required": [ + "bytesValue" + ] + }, + { + "required": [ + "timestampValue" + ] + }, + { + "required": [ + "durationValue" + ] + }, + { + "required": [ + "stringMapValue" + ] + } + ] + }, + "Attributes_StringMap": { + "type": "object", + "required": [ + "entries" + ], + "properties": { + "entries": { + "description": "Holds a set of name/value pairs.", + "type": "object", + "additionalProperties": { + "type": "string", + "format": "string" + } + } + } + } + } + } +} \ No newline at end of file