cuelang.org/go@v0.13.0/encoding/jsonschema/constraints_object.go (about) 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 jsonschema 16 17 import ( 18 "strings" 19 20 "cuelang.org/go/cue" 21 "cuelang.org/go/cue/ast" 22 "cuelang.org/go/cue/token" 23 "cuelang.org/go/internal" 24 ) 25 26 // Object constraints 27 28 func constraintPreserveUnknownFields(key string, n cue.Value, s *state) { 29 // x-kubernetes-preserve-unknown-fields stops the API server decoding 30 // step from pruning fields which are not specified in the validation 31 // schema. This affects fields recursively, but switches back to normal 32 // pruning behaviour if nested properties or additionalProperties are 33 // specified in the schema. This can either be true or undefined. False 34 // is forbidden. 35 // Note: by experimentation, "nested properties" means "within a schema 36 // within a nested property" not "within a schema that has the properties keyword". 37 if !s.boolValue(n) { 38 s.errf(n, "x-kubernetes-preserve-unknown-fields value may not be false") 39 return 40 } 41 // TODO check that it's specified on an object type. This requires 42 // either setting a bool (hasPreserveUnknownFields?) and checking 43 // later or making a new phase and placing this after "type" but 44 // before "allOf", because it's important that this value be 45 // passed down recursively to allOf and friends. 46 s.preserveUnknownFields = true 47 } 48 49 func constraintGroupVersionKind(key string, n cue.Value, s *state) { 50 // x-kubernetes-group-version-kind is used by Kubernetes schemas 51 // to indicate the required values of the apiVersion and kind fields. 52 items := s.listItems(key, n, false) 53 if len(items) != 1 { 54 // When there's more than one item, we _could_ generate 55 // a disjunction over apiVersion and kind but for now, we'll 56 // just ignore it. 57 // TODO implement support for multiple items 58 return 59 } 60 var group, version string 61 s.processMap(items[0], func(key string, n cue.Value) { 62 if strings.HasPrefix(key, "x-") { 63 // TODO are x- extension properties actually allowed in this context? 64 return 65 } 66 switch key { 67 case "group": 68 group, _ = s.strValue(n) 69 case "kind": 70 s.k8sResourceKind, _ = s.strValue(n) 71 case "version": 72 version, _ = s.strValue(n) 73 default: 74 s.errf(n, "unknown field %q in x-kubernetes-group-version-kind item", key) 75 } 76 }) 77 if s.k8sResourceKind == "" || version == "" { 78 s.errf(n, "x-kubernetes-group-version-kind needs both kind and version fields") 79 } 80 if group == "" { 81 s.k8sAPIVersion = version 82 } else { 83 s.k8sAPIVersion = group + "/" + version 84 } 85 } 86 87 func constraintAdditionalProperties(key string, n cue.Value, s *state) { 88 switch n.Kind() { 89 case cue.BoolKind: 90 if s.boolValue(n) { 91 s.openness = explicitlyOpen 92 } else { 93 if s.schemaVersion == VersionKubernetesCRD { 94 s.errf(n, "additionalProperties may not be set to false in a CRD schema") 95 return 96 } 97 s.openness = explicitlyClosed 98 } 99 _ = s.object(n) 100 101 case cue.StructKind: 102 obj := s.object(n) 103 if len(obj.Elts) == 0 { 104 obj.Elts = append(obj.Elts, &ast.Field{ 105 Label: ast.NewList(ast.NewIdent("string")), 106 Value: s.schema(n), 107 }) 108 s.openness = allFieldsCovered 109 return 110 } 111 // [!~(properties|patternProperties)]: schema 112 existing := append(s.patterns, excludeFields(obj.Elts)...) 113 expr, _ := s.schemaState(n, allTypes, func(s *state) { 114 s.preserveUnknownFields = false 115 }) 116 f := internal.EmbedStruct(ast.NewStruct(&ast.Field{ 117 Label: ast.NewList(ast.NewBinExpr(token.AND, existing...)), 118 Value: expr, 119 })) 120 obj.Elts = append(obj.Elts, f) 121 s.openness = allFieldsCovered 122 123 default: 124 s.errf(n, `value of "additionalProperties" must be an object or boolean`) 125 return 126 } 127 s.hasAdditionalProperties = true 128 } 129 130 func constraintDependencies(key string, n cue.Value, s *state) { 131 // Schema and property dependencies. 132 // TODO: the easiest implementation is with comprehensions. 133 // The nicer implementation is with disjunctions. This has to be done 134 // at the very end, replacing properties. 135 /* 136 *{ property?: _|_ } | { 137 property: _ 138 schema 139 } 140 */ 141 } 142 143 func constraintMaxProperties(key string, n cue.Value, s *state) { 144 pkg := s.addImport(n, "struct") 145 x := ast.NewCall(ast.NewSel(pkg, "MaxFields"), s.uint(n)) 146 s.add(n, objectType, x) 147 } 148 149 func constraintMinProperties(key string, n cue.Value, s *state) { 150 pkg := s.addImport(n, "struct") 151 x := ast.NewCall(ast.NewSel(pkg, "MinFields"), s.uint(n)) 152 s.add(n, objectType, x) 153 } 154 155 func constraintPatternProperties(key string, n cue.Value, s *state) { 156 if n.Kind() != cue.StructKind { 157 s.errf(n, `value of "patternProperties" must be an object, found %v`, n.Kind()) 158 } 159 obj := s.object(n) 160 existing := excludeFields(s.obj.Elts) 161 s.processMap(n, func(key string, n cue.Value) { 162 if !s.checkRegexp(n, key) { 163 return 164 } 165 166 // Record the pattern for potential use by 167 // additionalProperties because patternProperties are 168 // considered before additionalProperties. 169 s.patterns = append(s.patterns, 170 &ast.UnaryExpr{Op: token.NMAT, X: ast.NewString(key)}) 171 172 // We'll make a pattern constraint of the form: 173 // [pattern & !~(properties)]: schema 174 f := internal.EmbedStruct(ast.NewStruct(&ast.Field{ 175 Label: ast.NewList(ast.NewBinExpr( 176 token.AND, 177 append([]ast.Expr{&ast.UnaryExpr{Op: token.MAT, X: ast.NewString(key)}}, existing...)..., 178 )), 179 Value: s.schema(n), 180 })) 181 ast.SetRelPos(f, token.NewSection) 182 obj.Elts = append(obj.Elts, f) 183 }) 184 } 185 186 func constraintEmbeddedResource(key string, n cue.Value, s *state) { 187 // TODO: 188 // - should fail if type has not been specified as "object" 189 // - should fail if neither x-kubernetes-preserve-unknown-fields or properties have been specified 190 191 // Note: this runs in a phase before the properties keyword so 192 // that the embedded expression always comes first in the struct 193 // literal. 194 resourceDefinitionPath := cue.MakePath(cue.Hid("_embeddedResource", "_")) 195 obj := s.object(n) 196 197 // Generate a reference to a shared schema that all embedded resources 198 // can share. If it already exists, that's fine. 199 // TODO add an attribute to make it clear what's going on here 200 // when encoding a CRD from CUE? 201 s.builder.put(resourceDefinitionPath, ast.NewStruct( 202 "apiVersion", token.NOT, ast.NewIdent("string"), 203 "kind", token.NOT, ast.NewIdent("string"), 204 "metadata", token.OPTION, ast.NewStruct(&ast.Ellipsis{}), 205 ), nil) 206 refExpr, err := s.builder.getRef(resourceDefinitionPath) 207 if err != nil { 208 s.errf(n, `cannot get reference to embedded resource definition: %v`, err) 209 } else { 210 obj.Elts = append(obj.Elts, &ast.EmbedDecl{ 211 Expr: refExpr, 212 }) 213 } 214 s.allowedTypes &= cue.StructKind 215 } 216 217 func constraintProperties(key string, n cue.Value, s *state) { 218 obj := s.object(n) 219 220 if n.Kind() != cue.StructKind { 221 s.errf(n, `"properties" expected an object, found %v`, n.Kind()) 222 } 223 hasKind := false 224 hasAPIVersion := false 225 s.processMap(n, func(key string, n cue.Value) { 226 // property?: value 227 name := ast.NewString(key) 228 expr, state := s.schemaState(n, allTypes, func(s *state) { 229 s.preserveUnknownFields = false 230 }) 231 f := &ast.Field{Label: name, Value: expr} 232 if doc := state.comment(); doc != nil { 233 ast.SetComments(f, []*ast.CommentGroup{doc}) 234 } 235 f.Constraint = token.OPTION 236 if s.k8sResourceKind != "" && key == "kind" { 237 // Define a regular field with the specified kind value. 238 f.Constraint = token.ILLEGAL 239 f.Value = ast.NewString(s.k8sResourceKind) 240 hasKind = true 241 } 242 if s.k8sAPIVersion != "" && key == "apiVersion" { 243 // Define a regular field with the specified value. 244 f.Constraint = token.ILLEGAL 245 f.Value = ast.NewString(s.k8sAPIVersion) 246 hasAPIVersion = true 247 } 248 if len(obj.Elts) > 0 && len(f.Comments()) > 0 { 249 // TODO: change formatter such that either a NewSection on the 250 // field or doc comment will cause a new section. 251 ast.SetRelPos(f.Comments()[0], token.NewSection) 252 } 253 if state.deprecated { 254 switch expr.(type) { 255 case *ast.StructLit: 256 obj.Elts = append(obj.Elts, addTag(name, "deprecated", "")) 257 default: 258 f.Attrs = append(f.Attrs, internal.NewAttr("deprecated", "")) 259 } 260 } 261 obj.Elts = append(obj.Elts, f) 262 }) 263 // It's not entirely clear whether it's OK to have an x-kubernetes-group-version-kind 264 // keyword without the kind and apiVersion properties but be defensive 265 // and add them anyway even if they're not there already. 266 if s.k8sAPIVersion != "" && !hasAPIVersion { 267 obj.Elts = append(obj.Elts, &ast.Field{ 268 Label: ast.NewString("apiVersion"), 269 Value: ast.NewString(s.k8sAPIVersion), 270 }) 271 } 272 if s.k8sResourceKind != "" && !hasKind { 273 obj.Elts = append(obj.Elts, &ast.Field{ 274 Label: ast.NewString("kind"), 275 Value: ast.NewString(s.k8sResourceKind), 276 }) 277 } 278 s.hasProperties = true 279 } 280 281 func constraintPropertyNames(key string, n cue.Value, s *state) { 282 // [=~pattern]: _ 283 if names, _ := s.schemaState(n, cue.StringKind, nil); !isTop(names) { 284 x := ast.NewStruct(ast.NewList(names), top()) 285 s.add(n, objectType, x) 286 } 287 } 288 289 func constraintRequired(key string, n cue.Value, s *state) { 290 if n.Kind() != cue.ListKind { 291 s.errf(n, `value of "required" must be list of strings, found %v`, n.Kind()) 292 return 293 } 294 295 obj := s.object(n) 296 297 // Create field map 298 fields := map[string]*ast.Field{} 299 for _, d := range obj.Elts { 300 f, ok := d.(*ast.Field) 301 if !ok { 302 continue // Could be embedding? See cirrus.json 303 } 304 str, _, err := ast.LabelName(f.Label) 305 if err == nil { 306 fields[str] = f 307 } 308 } 309 310 for _, n := range s.listItems("required", n, true) { 311 str, ok := s.strValue(n) 312 f := fields[str] 313 if f == nil && ok { 314 f := &ast.Field{ 315 Label: ast.NewString(str), 316 Value: top(), 317 Constraint: token.NOT, 318 } 319 fields[str] = f 320 obj.Elts = append(obj.Elts, f) 321 continue 322 } 323 if f.Constraint == token.NOT { 324 s.errf(n, "duplicate required field %q", str) 325 } 326 f.Constraint = token.NOT 327 } 328 }