Marcel van Lohuizen | 9fad62f | 2019-08-13 01:39:10 +0200 | [diff] [blame] | 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 | } |