encoding/openapi: support nested defintitions
This perpares for Protobuf remodeling, where the nested
definitions of protobufs are are also nested within the
corresponding CUE types.
- Use Dereference
- Get comments from definitions, not fields, if applicable
- Allow embedded closed disjunctions.
- Allow nested types.
- Don't expand array element references.
Note that this removes the simplification in the openapi.cue
test: nested closed structs were not handled correctly by
this optimization.
Change-Id: Ie6adf44b37ac72139e21539094df5a728a309bd1
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5405
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/value.go b/cue/value.go
index 52abfd1..026080b 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -1260,7 +1260,7 @@
func appendDocComments(docs []*ast.CommentGroup, n ast.Node) []*ast.CommentGroup {
for _, c := range n.Comments() {
- if c.Position == 0 {
+ if c.Doc {
docs = append(docs, c)
}
}
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index be55c39..8ceeb2e 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -33,6 +33,7 @@
type buildContext struct {
inst *cue.Instance
+ instExt *cue.Instance
refPrefix string
path []string
@@ -80,6 +81,7 @@
c := buildContext{
inst: inst,
+ instExt: inst,
refPrefix: "components/schemas",
expandRefs: g.ExpandReferences,
structural: g.ExpandReferences,
@@ -132,7 +134,11 @@
for _, k := range external {
ext := c.externalRefs[k]
- c.schemas.Set(ext.ref, c.build(ext.ref, ext.value.Eval()))
+ 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)))
}
}
@@ -288,35 +294,12 @@
"default": 12,
}
-func (b *builder) resolve(v cue.Value) cue.Value {
- // Cycles are not allowed when expanding references. Right now we just
- // cap the depth of evaluation at 30.
- // TODO: do something more principled.
- const maxDepth = 30
- if b.ctx.evalDepth > maxDepth {
- b.failf(v, "maximum stack depth of %d reached", maxDepth)
- }
- b.ctx.evalDepth++
- defer func() { b.ctx.evalDepth-- }()
-
- switch op, a := v.Expr(); op {
- case cue.SelectorOp:
- field, _ := a[1].String()
- f, _ := b.resolve(a[0]).LookupField(field)
- v = cue.Value{}
- if !f.IsOptional {
- v = f.Value
- }
- }
- return v
-}
-
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 = b.resolve(v)
+ values = cue.Dereference(v)
count = 1
} else {
dedup := map[string]bool{}
@@ -328,7 +311,7 @@
case len(r) > 0:
ref := b.ctx.makeRef(p, r)
if ref == "" {
- v = b.resolve(v)
+ v = cue.Dereference(v)
break
}
if dedup[ref] {
@@ -370,27 +353,32 @@
}
}
- 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]
- }
+ _ = 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.
@@ -431,7 +419,7 @@
func appendSplit(a []cue.Value, splitBy cue.Op, v cue.Value) []cue.Value {
op, args := v.Expr()
- if op == cue.NoOp && len(args) > 0 {
+ 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...)
@@ -718,6 +706,9 @@
for i, _ := v.Fields(cue.Optional(true), cue.Definitions(true)); i.Next(); {
label := i.Label()
+ if b.ctx.isInternal(label) {
+ continue
+ }
var core *builder
if b.core != nil {
core = b.core.properties[label]
@@ -725,7 +716,10 @@
schema := b.schema(core, label, i.Value())
switch {
case i.IsDefinition():
- ref := strings.Join(append(b.ctx.path, label), ".")
+ 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)
diff --git a/encoding/openapi/testdata/builtins.json b/encoding/openapi/testdata/builtins.json
index 91db4e7..8d43e35 100644
--- a/encoding/openapi/testdata/builtins.json
+++ b/encoding/openapi/testdata/builtins.json
@@ -5,7 +5,6 @@
"components": {
"schemas": {
"Duration": {
- "description": "This is not an OpenAPI type and has no format. In this case\nwe map to a type so that it can be documented properly (without\nrepeating it).",
"type": "string"
},
"MyStruct": {
diff --git a/encoding/openapi/testdata/nested.cue b/encoding/openapi/testdata/nested.cue
index f1343e0..a95b2b0 100644
--- a/encoding/openapi/testdata/nested.cue
+++ b/encoding/openapi/testdata/nested.cue
@@ -4,5 +4,8 @@
T :: int
a?: T
- b?: T
+
+ {b?: T}
+
+ c?: [...T]
}
diff --git a/encoding/openapi/testdata/nested.json b/encoding/openapi/testdata/nested.json
index d390c64..c392b15 100644
--- a/encoding/openapi/testdata/nested.json
+++ b/encoding/openapi/testdata/nested.json
@@ -10,6 +10,12 @@
"a": {
"$ref": "#/components/schemas/Struct.T"
},
+ "c": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Struct.T"
+ }
+ },
"b": {
"$ref": "#/components/schemas/Struct.T"
}
diff --git a/encoding/openapi/testdata/oneof-funcs.json b/encoding/openapi/testdata/oneof-funcs.json
index 19c7fe3..b61f51b 100644
--- a/encoding/openapi/testdata/oneof-funcs.json
+++ b/encoding/openapi/testdata/oneof-funcs.json
@@ -16,59 +16,123 @@
"type": "integer"
}
},
- "oneOf": [
+ "allOf": [
{
- "not": {
- "anyOf": [
- {
- "required": [
- "b"
- ],
- "properties": {
- "b": {
- "description": "Randomly picked description from a set of size one.",
- "type": "integer"
+ "oneOf": [
+ {
+ "not": {
+ "anyOf": [
+ {
+ "required": [
+ "b"
+ ],
+ "properties": {
+ "b": {
+ "$ref": "#/components/schemas/EMBED_T"
+ }
+ }
+ },
+ {
+ "required": [
+ "c"
+ ],
+ "properties": {
+ "c": {
+ "description": "Randomly picked description from a set of size one.",
+ "type": "integer"
+ }
+ }
}
- }
- },
- {
- "required": [
- "c"
- ],
- "properties": {
- "c": {
- "description": "Randomly picked description from a set of size one.",
- "type": "integer"
- }
+ ]
+ }
+ },
+ {
+ "required": [
+ "b"
+ ],
+ "properties": {
+ "b": {
+ "$ref": "#/components/schemas/EMBED_T"
}
}
- ]
- }
+ },
+ {
+ "required": [
+ "c"
+ ],
+ "properties": {
+ "c": {
+ "description": "Randomly picked description from a set of size one.",
+ "type": "integer"
+ }
+ }
+ }
+ ]
},
{
- "required": [
- "b"
- ],
- "properties": {
- "b": {
- "description": "Randomly picked description from a set of size one.",
- "type": "integer"
+ "oneOf": [
+ {
+ "not": {
+ "anyOf": [
+ {
+ "required": [
+ "d"
+ ],
+ "properties": {
+ "d": {
+ "$ref": "#/components/schemas/EMBED_T"
+ }
+ }
+ },
+ {
+ "required": [
+ "e"
+ ],
+ "properties": {
+ "e": {
+ "description": "Randomly picked description from a set of size one.",
+ "type": "integer"
+ }
+ }
+ }
+ ]
+ }
+ },
+ {
+ "required": [
+ "d"
+ ],
+ "properties": {
+ "d": {
+ "$ref": "#/components/schemas/EMBED_T"
+ }
+ }
+ },
+ {
+ "required": [
+ "e"
+ ],
+ "properties": {
+ "e": {
+ "description": "Randomly picked description from a set of size one.",
+ "type": "integer"
+ }
+ }
}
- }
- },
- {
- "required": [
- "c"
- ],
- "properties": {
- "c": {
- "description": "Randomly picked description from a set of size one.",
- "type": "integer"
- }
- }
+ ]
}
]
},
+ "EMBED_T": {
+ "description": "Randomly picked description from a set of size one.",
+ "type": "object",
+ "properties": {
+ "b": {
+ "description": "Randomly picked description from a set of size one.",
+ "type": "integer"
+ }
+ }
+ },
"FOO": {
"description": "Randomly picked description from a set of size one.",
"type": "object",
diff --git a/encoding/openapi/testdata/oneof-resolve.json b/encoding/openapi/testdata/oneof-resolve.json
index 038f2ac..2b894b0 100644
--- a/encoding/openapi/testdata/oneof-resolve.json
+++ b/encoding/openapi/testdata/oneof-resolve.json
@@ -13,42 +13,100 @@
"a": {
"type": "integer"
},
- "b": {
+ "d": {
+ "type": "object",
+ "properties": {
+ "b": {
+ "type": "integer"
+ }
+ }
+ },
+ "e": {
"type": "integer"
},
+ "b": {
+ "type": "object",
+ "properties": {
+ "b": {
+ "type": "integer"
+ }
+ }
+ },
"c": {
"type": "integer"
}
},
- "oneOf": [
+ "allOf": [
{
- "not": {
- "anyOf": [
- {
- "required": [
- "b"
- ]
- },
- {
- "required": [
- "c"
+ "oneOf": [
+ {
+ "not": {
+ "anyOf": [
+ {
+ "required": [
+ "b"
+ ]
+ },
+ {
+ "required": [
+ "c"
+ ]
+ }
]
}
- ]
- }
- },
- {
- "required": [
- "b"
+ },
+ {
+ "required": [
+ "b"
+ ]
+ },
+ {
+ "required": [
+ "c"
+ ]
+ }
]
},
{
- "required": [
- "c"
+ "oneOf": [
+ {
+ "not": {
+ "anyOf": [
+ {
+ "required": [
+ "d"
+ ]
+ },
+ {
+ "required": [
+ "e"
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "required": [
+ "d"
+ ]
+ },
+ {
+ "required": [
+ "e"
+ ]
+ }
]
}
]
},
+ "Embed.T": {
+ "type": "object",
+ "properties": {
+ "b": {
+ "type": "integer"
+ }
+ }
+ },
"Foo": {
"type": "object",
"required": [
diff --git a/encoding/openapi/testdata/oneof.cue b/encoding/openapi/testdata/oneof.cue
index 6fa37d4..a78285d 100644
--- a/encoding/openapi/testdata/oneof.cue
+++ b/encoding/openapi/testdata/oneof.cue
@@ -47,9 +47,15 @@
a?: int
close({}) |
- close({b: int}) |
+ close({b: T}) |
close({c: int})
+ T :: {b?: int}
+
+ close({}) |
+ close({d: T}) |
+ close({e: int})
+
// TODO: maybe support builtin to write this as
// oneof({},
// {b: int},
diff --git a/encoding/openapi/testdata/oneof.json b/encoding/openapi/testdata/oneof.json
index 48ce218..6f0c9f6 100644
--- a/encoding/openapi/testdata/oneof.json
+++ b/encoding/openapi/testdata/oneof.json
@@ -11,55 +11,117 @@
"type": "integer"
}
},
- "oneOf": [
+ "allOf": [
{
- "not": {
- "anyOf": [
- {
- "required": [
- "b"
- ],
- "properties": {
- "b": {
- "type": "integer"
+ "oneOf": [
+ {
+ "not": {
+ "anyOf": [
+ {
+ "required": [
+ "b"
+ ],
+ "properties": {
+ "b": {
+ "$ref": "#/components/schemas/Embed.T"
+ }
+ }
+ },
+ {
+ "required": [
+ "c"
+ ],
+ "properties": {
+ "c": {
+ "type": "integer"
+ }
+ }
}
- }
- },
- {
- "required": [
- "c"
- ],
- "properties": {
- "c": {
- "type": "integer"
- }
+ ]
+ }
+ },
+ {
+ "required": [
+ "b"
+ ],
+ "properties": {
+ "b": {
+ "$ref": "#/components/schemas/Embed.T"
}
}
- ]
- }
+ },
+ {
+ "required": [
+ "c"
+ ],
+ "properties": {
+ "c": {
+ "type": "integer"
+ }
+ }
+ }
+ ]
},
{
- "required": [
- "b"
- ],
- "properties": {
- "b": {
- "type": "integer"
+ "oneOf": [
+ {
+ "not": {
+ "anyOf": [
+ {
+ "required": [
+ "d"
+ ],
+ "properties": {
+ "d": {
+ "$ref": "#/components/schemas/Embed.T"
+ }
+ }
+ },
+ {
+ "required": [
+ "e"
+ ],
+ "properties": {
+ "e": {
+ "type": "integer"
+ }
+ }
+ }
+ ]
+ }
+ },
+ {
+ "required": [
+ "d"
+ ],
+ "properties": {
+ "d": {
+ "$ref": "#/components/schemas/Embed.T"
+ }
+ }
+ },
+ {
+ "required": [
+ "e"
+ ],
+ "properties": {
+ "e": {
+ "type": "integer"
+ }
+ }
}
- }
- },
- {
- "required": [
- "c"
- ],
- "properties": {
- "c": {
- "type": "integer"
- }
- }
+ ]
}
]
},
+ "Embed.T": {
+ "type": "object",
+ "properties": {
+ "b": {
+ "type": "integer"
+ }
+ }
+ },
"Foo": {
"type": "object",
"required": [
diff --git a/encoding/openapi/testdata/openapi-norefs.json b/encoding/openapi/testdata/openapi-norefs.json
index d39a589..2839706 100644
--- a/encoding/openapi/testdata/openapi-norefs.json
+++ b/encoding/openapi/testdata/openapi-norefs.json
@@ -177,6 +177,11 @@
"oneOf": [
{
"required": [
+ "a"
+ ]
+ },
+ {
+ "required": [
"b"
]
},
diff --git a/encoding/openapi/testdata/openapi.json b/encoding/openapi/testdata/openapi.json
index ef7d41c..8f8502f 100644
--- a/encoding/openapi/testdata/openapi.json
+++ b/encoding/openapi/testdata/openapi.json
@@ -166,6 +166,16 @@
"oneOf": [
{
"required": [
+ "a"
+ ],
+ "properties": {
+ "a": {
+ "type": "number"
+ }
+ }
+ },
+ {
+ "required": [
"b"
],
"properties": {
diff --git a/encoding/openapi/types.go b/encoding/openapi/types.go
index 3a82362..39f5c59 100644
--- a/encoding/openapi/types.go
+++ b/encoding/openapi/types.go
@@ -16,6 +16,7 @@
import (
"fmt"
+ "strings"
"github.com/cockroachdb/apd/v2"
@@ -38,7 +39,6 @@
"bytes": "binary",
"time.Time": "dateTime",
- "time.Time ()": "dateTime",
`time.Format ("2006-01-02")`: "date",
// TODO: if a format is more strict (e.g. using zeros instead of nines
@@ -54,11 +54,26 @@
default:
return ""
}
- b, err := format.Node(v.Syntax(cue.Final()))
- if err != nil {
- return ""
+ var expr, arg string
+ op, a := v.Expr()
+ if op == cue.CallOp {
+ v = a[0]
+ if len(a) == 2 {
+ arg = fmt.Sprintf(" (%s)", a[1].Eval())
+ }
}
- if s, ok := cueToOpenAPI[string(b)]; ok {
+ if inst, ref := v.Reference(); len(ref) > 0 {
+ expr = inst.ImportPath + "." + strings.Join(ref, ".")
+ expr += arg
+ } else {
+ // TODO: have some function to extract normalized builtin types.
+ b, err := format.Node(v.Syntax(cue.Final()))
+ if err != nil {
+ return ""
+ }
+ expr = string(b)
+ }
+ if s, ok := cueToOpenAPI[expr]; ok {
return s
}
s := fmt.Sprint(v)