blob: 28b935f27f769d9127a9a80c110132070727269a [file] [log] [blame]
Marcel van Lohuizen9fad62f2019-08-13 01:39:10 +02001// 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)
49
50// newCoreBuilder returns a builder that represents a structural schema.
51func 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.
58func (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.
106func (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
149func (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}