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
15package 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
46import (
47 "cuelang.org/go/cue"
48 "cuelang.org/go/cue/ast"
49)
50
51// newCoreBuilder returns a builder that represents a structural schema.
52func newCoreBuilder(c *buildContext) *builder {
53 b := newRootBuilder(c)
54 b.properties = map[string]*builder{}
55 return b
56}
57
58func (b *builder) coreSchemaWithName(name cue.Selector) *ast.StructLit {
59 oldPath := b.ctx.path
60 b.ctx.path = append(b.ctx.path, name)
61 s := b.coreSchema()
62 b.ctx.path = oldPath
63 return s
64}
65
66// coreSchema creates the core part of a structural OpenAPI.
67func (b *builder) coreSchema() *ast.StructLit {
68 switch b.kind {
69 case cue.ListKind:
70 if b.items != nil {
71 b.setType("array", "")
72 schema := b.items.coreSchemaWithName(cue.AnyString)
73 b.setSingle("items", schema, false)
74 }
75
76 case cue.StructKind:
77 p := &orderedMap{}
78 for _, k := range b.keys {
79 sub := b.properties[k]
80 p.setExpr(k, sub.coreSchemaWithName(cue.Str(k)))
81 }
82 if p.len() > 0 || b.items != nil {
83 b.setType("object", "")
84 }
85 if p.len() > 0 {
86 b.setSingle("properties", (*ast.StructLit)(p), false)
87 }
88 // TODO: in Structural schema only one of these is allowed.
89 if b.items != nil {
90 schema := b.items.coreSchemaWithName(cue.AnyString)
91 b.setSingle("additionalProperties", schema, false)
92 }
93 }
94
95 // If there was only a single value associated with this node, we can
96 // safely assume there were no disjunctions etc. In structural mode this
97 // is the only chance we get to set certain properties.
98 if len(b.values) == 1 {
99 return b.fillSchema(b.values[0])
100 }
101
102 // TODO: do type analysis if we have multiple values and piece out more
103 // information that applies to all possible instances.
104
105 return b.finish()
106}
107
108// buildCore collects the CUE values for the structural OpenAPI tree.
109// To this extent, all fields of both conjunctions and disjunctions are
110// collected in a single properties map.
111func (b *builder) buildCore(v cue.Value) {
112 b.pushNode(v)
113 defer b.popNode()
114
115 if !b.ctx.expandRefs {
116 _, r := v.ReferencePath()
117 if len(r.Selectors()) > 0 {
118 return
119 }
120 }
121 b.getDoc(v)
122 format := extractFormat(v)
123 if format != "" {
124 b.format = format
125 } else {
126 v = v.Eval()
127 b.kind = v.IncompleteKind()
128
129 switch b.kind {
130 case cue.StructKind:
131 if typ := v.LookupPath(cue.MakePath(cue.AnyString)); typ.Exists() {
132 if !b.checkCycle(typ) {
133 return
134 }
135 if b.items == nil {
136 b.items = newCoreBuilder(b.ctx)
137 }
138 b.items.buildCore(typ)
139 }
140 b.buildCoreStruct(v)
141
142 case cue.ListKind:
143 if typ := v.LookupPath(cue.MakePath(cue.AnyIndex)); typ.Exists() {
144 if !b.checkCycle(typ) {
145 return
146 }
147 if b.items == nil {
148 b.items = newCoreBuilder(b.ctx)
149 }
150 b.items.buildCore(typ)
151 }
152 }
153 }
154
155 for _, bv := range b.values {
156 if bv.Equals(v) {
157 return
158 }
159 }
160 b.values = append(b.values, v)
161}
162
163func (b *builder) buildCoreStruct(v cue.Value) {
164 op, args := v.Expr()
165 switch op {
166 case cue.OrOp, cue.AndOp:
167 for _, v := range args {
168 b.buildCore(v)
169 }
170 }
171 for i, _ := v.Fields(cue.Optional(true), cue.Hidden(false)); i.Next(); {
172 label := i.Selector().Unquoted()
173 sub, ok := b.properties[label]
174 if !ok {
175 sub = newCoreBuilder(b.ctx)
176 b.properties[label] = sub
177 b.keys = append(b.keys, label)
178 }
179 sub.buildCore(i.Value())
180 }
181}