Skip to content

Commit 9fad62f

Browse files
committed
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 <[email protected]> Reviewed-by: Marcel van Lohuizen <[email protected]>
1 parent 35abfa7 commit 9fad62f

File tree

13 files changed

+909
-396
lines changed

13 files changed

+909
-396
lines changed

encoding/openapi/build.go

Lines changed: 202 additions & 72 deletions
Large diffs are not rendered by default.

encoding/openapi/crd.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright 2019 CUE Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package openapi
16+
17+
// This file contains functionality for structural schema, a subset of OpenAPI
18+
// used for CRDs.
19+
//
20+
// See https://kubernetes.io/blog/2019/06/20/crd-structural-schema/ for details.
21+
//
22+
// Insofar definitions are compatible, openapi normalizes to structural whenever
23+
// possible.
24+
//
25+
// A core structural schema is only made out of the following fields:
26+
//
27+
// - properties
28+
// - items
29+
// - additionalProperties
30+
// - type
31+
// - nullable
32+
// - title
33+
// - descriptions.
34+
//
35+
// Where the types must be defined for all fields.
36+
//
37+
// In addition, the value validations constraints may be used as defined in
38+
// OpenAPI, with the restriction that
39+
// - within the logical constraints anyOf, allOf, oneOf, and not
40+
// additionalProperties, type, nullable, title, and description may not be used.
41+
// - all mentioned fields must be defined in the core schema.
42+
//
43+
// It appears that CRDs do not allow references.
44+
//
45+
46+
import (
47+
"cuelang.org/go/cue"
48+
)
49+
50+
// newCoreBuilder returns a builder that represents a structural schema.
51+
func newCoreBuilder(c *buildContext) *builder {
52+
b := newRootBuilder(c)
53+
b.properties = map[string]*builder{}
54+
return b
55+
}
56+
57+
// coreSchema creates the core part of a structural OpenAPI.
58+
func (b *builder) coreSchema(name string) *oaSchema {
59+
oldPath := b.ctx.path
60+
b.ctx.path = append(b.ctx.path, name)
61+
defer func() { b.ctx.path = oldPath }()
62+
63+
switch b.kind {
64+
case cue.ListKind:
65+
if b.items != nil {
66+
b.setType("array", "")
67+
schema := b.items.coreSchema("*")
68+
b.setSingle("items", schema, false)
69+
}
70+
71+
case cue.StructKind:
72+
p := &OrderedMap{}
73+
for _, k := range b.keys {
74+
sub := b.properties[k]
75+
p.Set(k, sub.coreSchema(k))
76+
}
77+
if len(p.kvs) > 0 || b.items != nil {
78+
b.setType("object", "")
79+
}
80+
if len(p.kvs) > 0 {
81+
b.setSingle("properties", p, false)
82+
}
83+
// TODO: in Structural schema only one of these is allowed.
84+
if b.items != nil {
85+
schema := b.items.coreSchema("*")
86+
b.setSingle("additionalProperties", schema, false)
87+
}
88+
}
89+
90+
// If there was only a single value associated with this node, we can
91+
// safely assume there were no disjunctions etc. In structural mode this
92+
// is the only chance we get to set certain properties.
93+
if len(b.values) == 1 {
94+
return b.fillSchema(b.values[0])
95+
}
96+
97+
// TODO: do type analysis if we have multiple values and piece out more
98+
// information that applies to all possible instances.
99+
100+
return b.finish()
101+
}
102+
103+
// buildCore collects the CUE values for the structural OpenAPI tree.
104+
// To this extent, all fields of both conjunctions and disjunctions are
105+
// collected in a single properties map.
106+
func (b *builder) buildCore(v cue.Value) {
107+
if !b.ctx.expandRefs {
108+
_, r := v.Reference()
109+
if len(r) > 0 {
110+
return
111+
}
112+
}
113+
b.getDoc(v)
114+
format := extractFormat(v)
115+
if format != "" {
116+
b.format = format
117+
} else {
118+
v = v.Eval()
119+
b.kind = v.IncompleteKind() &^ cue.BottomKind
120+
121+
switch b.kind {
122+
case cue.StructKind:
123+
if typ, ok := v.Elem(); ok {
124+
if b.items == nil {
125+
b.items = newCoreBuilder(b.ctx)
126+
}
127+
b.items.buildCore(typ)
128+
}
129+
b.buildCoreStruct(v)
130+
131+
case cue.ListKind:
132+
if typ, ok := v.Elem(); ok {
133+
if b.items == nil {
134+
b.items = newCoreBuilder(b.ctx)
135+
}
136+
b.items.buildCore(typ)
137+
}
138+
}
139+
}
140+
141+
for _, bv := range b.values {
142+
if bv.Equals(v) {
143+
return
144+
}
145+
}
146+
b.values = append(b.values, v)
147+
}
148+
149+
func (b *builder) buildCoreStruct(v cue.Value) {
150+
op, args := v.Expr()
151+
switch op {
152+
case cue.OrOp, cue.AndOp:
153+
for _, v := range args {
154+
b.buildCore(v)
155+
}
156+
}
157+
for i, _ := v.Fields(cue.Optional(true), cue.Hidden(false)); i.Next(); {
158+
label := i.Label()
159+
sub, ok := b.properties[label]
160+
if !ok {
161+
sub = newCoreBuilder(b.ctx)
162+
b.properties[label] = sub
163+
b.keys = append(b.keys, label)
164+
}
165+
sub.buildCore(i.Value())
166+
}
167+
}

encoding/openapi/openapi_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ func TestParseDefinitions(t *testing.T) {
3939
in, out string
4040
config *Config
4141
}{{
42+
"structural.cue",
43+
"structural.json",
44+
resolveRefs,
45+
}, {
4246
"simple.cue",
4347
"simple.json",
4448
resolveRefs,
@@ -138,3 +142,27 @@ func TestParseDefinitions(t *testing.T) {
138142
})
139143
}
140144
}
145+
146+
// This is for debugging purposes. Do not remove.
147+
func TestX(t *testing.T) {
148+
t.Skip()
149+
150+
var r cue.Runtime
151+
inst, err := r.Compile("test", `
152+
AnyField: "any value"
153+
`)
154+
if err != nil {
155+
t.Fatal(err)
156+
}
157+
158+
b, err := Gen(inst, &Config{
159+
ExpandReferences: true,
160+
})
161+
if err != nil {
162+
t.Fatal(err)
163+
}
164+
165+
var out = &bytes.Buffer{}
166+
_ = json.Indent(out, b, "", " ")
167+
t.Error(out.String())
168+
}

encoding/openapi/testdata/array.json

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
"bar": {
1010
"type": "array",
1111
"items": {
12-
"default": "1",
12+
"type": "string",
1313
"enum": [
1414
"1",
1515
"2",
1616
"3"
17-
]
17+
],
18+
"default": "1"
1819
}
1920
},
2021
"foo": {
@@ -28,12 +29,13 @@
2829
"e": {
2930
"type": "array",
3031
"items": {
31-
"default": "1",
32+
"type": "string",
3233
"enum": [
3334
"1",
3435
"2",
3536
"3"
36-
]
37+
],
38+
"default": "1"
3739
}
3840
}
3941
}
@@ -52,12 +54,13 @@
5254
},
5355
"MyEnum": {
5456
"description": "MyEnum",
55-
"default": "1",
57+
"type": "string",
5658
"enum": [
5759
"1",
5860
"2",
5961
"3"
60-
]
62+
],
63+
"default": "1"
6164
},
6265
"MyStruct": {
6366
"description": "MyStruct",
@@ -69,12 +72,13 @@
6972
"e": {
7073
"type": "array",
7174
"items": {
72-
"default": "1",
75+
"type": "string",
7376
"enum": [
7477
"1",
7578
"2",
7679
"3"
77-
]
80+
],
81+
"default": "1"
7882
}
7983
}
8084
}

encoding/openapi/testdata/nums.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,11 @@
1010
"neq": {
1111
"type": "number",
1212
"not": {
13-
"type": "number",
1413
"allOff": [
1514
{
16-
"type": "number",
1715
"minimum": 4
1816
},
1917
{
20-
"type": "number",
2118
"maximum": 4
2219
}
2320
]

encoding/openapi/testdata/oneof-funcs.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
"schemas": {
99
"MYSTRING": {
1010
"description": "Randomly picked description from a set of size one.",
11+
"type": "object",
1112
"oneOf": [
1213
{
13-
"type": "object",
1414
"required": [
1515
"exact"
1616
],
@@ -23,7 +23,6 @@
2323
}
2424
},
2525
{
26-
"type": "object",
2726
"required": [
2827
"regex"
2928
],

0 commit comments

Comments
 (0)