encoding/openapi: implement structural schema
See https://kubernetes.io/blog/2019/06/20/crd-structural-schema/
This is needed to make generated schema compliant with CRDs.
Structural schema are momentarily enabled by requesting to
expand references. Even when not expanding, the generator
will strive to normalize the schema somewhat, however.
Change-Id: I36fc8bc0d0e41d1b47b8bed55462ab9d07cfc26f
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2803
Reviewed-by: Jason Wang <jasonwzm@google.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index c491a1c..6a32f5d 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -36,6 +36,7 @@
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 @@
inst: inst,
refPrefix: "components/schemas",
expandRefs: g.ExpandReferences,
+ structural: g.ExpandReferences,
nameFunc: g.ReferenceFunc,
descFunc: g.DescriptionFunc,
schemas: &OrderedMap{},
@@ -137,7 +139,7 @@
}
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) 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)
}
- if len(doc) > 0 {
- str := strings.TrimSpace(strings.Join(doc, "\n\n"))
- schema.Set("description", 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", 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 @@
"minLength": 16,
"maxLength": 15,
"items": 14,
+ "enum": 13,
+ "default": 12,
}
func (b *builder) resolve(v cue.Value) cue.Value {
@@ -302,8 +336,9 @@
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 @@
}
fallthrough
default:
- b.setFilter("Schema", "default", v)
+ if !b.isNonCore() {
+ b.setFilter("Schema", "default", v)
+ }
}
}
return isRef
@@ -418,34 +455,64 @@
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 @@
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 @@
b.setFilter("Schema", "required", required)
}
- properties := &OrderedMap{}
- for i, _ := v.Fields(cue.Optional(true), cue.Hidden(false)); i.Next(); {
- properties.Set(i.Label(), b.schema(i.Label(), i.Value()))
+ var properties *OrderedMap
+ if b.singleFields != nil {
+ properties = b.singleFields.getMap("properties")
}
- if len(properties.kvs) > 0 {
- b.set("properties", properties)
+ hasProps := properties != nil
+ if !hasProps {
+ properties = &OrderedMap{}
}
- if t, ok := v.Elem(); ok {
- b.setFilter("Schema", "additionalProperties", b.schema("*", t))
+ for i, _ := v.Fields(cue.Optional(true), cue.Hidden(false)); i.Next(); {
+ 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 !hasProps && len(properties.kvs) > 0 {
+ b.setSingle("properties", properties, false)
+ }
+
+ 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 @@
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 @@
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 @@
}
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 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 @@
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 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 @@
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) kv(key string, value interface{}) *oaSchema {
constraint := &OrderedMap{}
- setType(constraint, b)
constraint.Set(key, value)
return constraint
}
@@ -927,24 +1053,28 @@
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 0000000..28b935f
--- /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 1459527..ea634ac 100644
--- a/encoding/openapi/openapi_test.go
+++ b/encoding/openapi/openapi_test.go
@@ -39,6 +39,10 @@
in, out string
config *Config
}{{
+ "structural.cue",
+ "structural.json",
+ resolveRefs,
+ }, {
"simple.cue",
"simple.json",
resolveRefs,
@@ -138,3 +142,27 @@
})
}
}
+
+// 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 570d659..5a9136a 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 fb6efba..69874fe 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 dd0755e..157b53c 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 217dc9f..629e0dd 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 2781b7b..b2c0c97 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 658290e..0c7e922 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 c60e19e..a8b888a 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 d62bfff..4b8123b 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 0000000..f1bb1c6
--- /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<string,string>)
+}
diff --git a/encoding/openapi/testdata/structural.json b/encoding/openapi/testdata/structural.json
new file mode 100644
index 0000000..b1beee2
--- /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