cuelang.org/go@v0.13.0/encoding/jsonschema/constraints_generic.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 "errors" 19 "fmt" 20 "net/url" 21 "strings" 22 23 "cuelang.org/go/cue" 24 "cuelang.org/go/cue/ast" 25 "cuelang.org/go/cue/token" 26 ) 27 28 // Generic constraints 29 30 func constraintAddDefinitions(key string, n cue.Value, s *state) { 31 if n.Kind() != cue.StructKind { 32 s.errf(n, `%q expected an object, found %s`, key, n.Kind()) 33 } 34 35 s.processMap(n, func(key string, n cue.Value) { 36 // Ensure that we are going to make a definition 37 // for this node. 38 s.ensureDefinition(n) 39 s.schema(n) 40 }) 41 } 42 43 func constraintComment(key string, n cue.Value, s *state) { 44 } 45 46 func constraintConst(key string, n cue.Value, s *state) { 47 s.all.add(n, s.constValue(n)) 48 s.allowedTypes &= n.Kind() 49 s.knownTypes &= n.Kind() 50 } 51 52 func constraintDefault(key string, n cue.Value, s *state) { 53 // TODO make the default value available in a separate 54 // template-like CUE value outside of the usual schema output. 55 } 56 57 func constraintDeprecated(key string, n cue.Value, s *state) { 58 if s.boolValue(n) { 59 s.deprecated = true 60 } 61 } 62 63 func constraintDescription(key string, n cue.Value, s *state) { 64 s.description, _ = s.strValue(n) 65 } 66 67 func constraintEnum(key string, n cue.Value, s *state) { 68 var a []ast.Expr 69 var types cue.Kind 70 for _, x := range s.listItems("enum", n, true) { 71 if (s.allowedTypes & x.Kind()) == 0 { 72 // Enum value is redundant because it's 73 // not in the allowed type set. 74 continue 75 } 76 a = append(a, s.constValue(x)) 77 types |= x.Kind() 78 } 79 s.knownTypes &= types 80 s.allowedTypes &= types 81 if len(a) > 0 { 82 s.all.add(n, ast.NewBinExpr(token.OR, a...)) 83 } 84 } 85 86 func constraintExamples(key string, n cue.Value, s *state) { 87 if n.Kind() != cue.ListKind { 88 s.errf(n, `value of "examples" must be an array, found %v`, n.Kind()) 89 } 90 } 91 92 func constraintNullable(key string, n cue.Value, s *state) { 93 null := ast.NewNull() 94 setPos(null, n) 95 s.nullable = null 96 } 97 98 func constraintRef(key string, n cue.Value, s *state) { 99 u := s.resolveURI(n) 100 if u == nil { 101 return 102 } 103 schemaRoot := s.schemaRoot() 104 if u.Fragment == "" && schemaRoot.isRoot && sameSchemaRoot(u, schemaRoot.id) { 105 // It's a reference to the root of the schema being 106 // generated. This never maps to something different. 107 s.all.add(n, s.refExpr(n, "", cue.Path{})) 108 return 109 } 110 importPath, path, err := cueLocationForRef(s, n, u, schemaRoot) 111 if err != nil { 112 s.errf(n, "%v", err) 113 return 114 } 115 if e := s.refExpr(n, importPath, path); e != nil { 116 s.all.add(n, e) 117 } 118 } 119 120 func cueLocationForRef(s *state, n cue.Value, u *url.URL, schemaRoot *state) (importPath string, path cue.Path, err error) { 121 if ds, ok := s.defs[u.String()]; ok { 122 // We already know about the schema, so use the information that's stored for it. 123 return ds.importPath, ds.path, nil 124 } 125 loc := SchemaLoc{ 126 ID: u, 127 } 128 var base cue.Value 129 isAnchor := u.Fragment != "" && !strings.HasPrefix(u.Fragment, "/") 130 if !isAnchor { 131 // It's a JSON pointer reference. 132 if sameSchemaRoot(u, s.rootID) { 133 base = s.root 134 } else if sameSchemaRoot(u, schemaRoot.id) { 135 // it's within the current schema. 136 base = schemaRoot.pos 137 } 138 if base.Exists() { 139 target, err := lookupJSONPointer(schemaRoot.pos, u.Fragment) 140 if err != nil { 141 if errors.Is(err, errRefNotFound) { 142 return "", cue.Path{}, fmt.Errorf("reference to non-existent schema") 143 } 144 return "", cue.Path{}, fmt.Errorf("invalid JSON Pointer: %v", err) 145 } 146 if ds := s.defForValue.get(target); ds != nil { 147 // There's a definition in place for the value, which gives 148 // us our answer. 149 return ds.importPath, ds.path, nil 150 } 151 s.ensureDefinition(target) 152 loc.IsLocal = true 153 loc.Path = relPath(target, s.root) 154 } 155 } 156 importPath, path, err = s.cfg.MapRef(loc) 157 if err != nil { 158 return "", cue.Path{}, fmt.Errorf("cannot determine CUE location for JSON Schema location %v: %v", loc, err) 159 } 160 // TODO we'd quite like to avoid invoking MapRef many times 161 // for the same reference, but in general we don't necessily know 162 // the canonical URI of the schema until we've done at least one pass. 163 // There are potentially ways to do it, but leave it for now in favor 164 // of simplicity. 165 return importPath, path, nil 166 } 167 168 func constraintTitle(key string, n cue.Value, s *state) { 169 s.title, _ = s.strValue(n) 170 } 171 172 func constraintIntOrString(key string, n cue.Value, s *state) { 173 // See x-kubernetes-int-or-string in 174 // https://kubernetes.io/docs/reference/kubernetes-api/extend-resources/custom-resource-definition-v1/#JSONSchemaProps. 175 s.setTypeUsed(n, stringType) 176 s.setTypeUsed(n, numType) 177 s.add(n, numType, ast.NewIdent("int")) 178 s.allowedTypes &= cue.StringKind | cue.IntKind 179 } 180 181 func constraintType(key string, n cue.Value, s *state) { 182 var types cue.Kind 183 set := func(n cue.Value) { 184 str, ok := s.strValue(n) 185 if !ok { 186 s.errf(n, "type value should be a string") 187 } 188 switch str { 189 case "null": 190 types |= cue.NullKind 191 s.setTypeUsed(n, nullType) 192 // TODO: handle OpenAPI restrictions. 193 case "boolean": 194 types |= cue.BoolKind 195 s.setTypeUsed(n, boolType) 196 case "string": 197 types |= cue.StringKind 198 s.setTypeUsed(n, stringType) 199 case "number": 200 types |= cue.NumberKind 201 s.setTypeUsed(n, numType) 202 case "integer": 203 types |= cue.IntKind 204 s.setTypeUsed(n, numType) 205 s.add(n, numType, ast.NewIdent("int")) 206 case "array": 207 types |= cue.ListKind 208 s.setTypeUsed(n, arrayType) 209 // For OpenAPI, specifically keep track of whether type is array 210 // so we can mandate the "items" keyword. 211 s.isArray = true 212 case "object": 213 types |= cue.StructKind 214 s.setTypeUsed(n, objectType) 215 216 default: 217 s.errf(n, "unknown type %q", n) 218 } 219 } 220 221 switch n.Kind() { 222 case cue.StringKind: 223 set(n) 224 case cue.ListKind: 225 if openAPILike.contains(s.schemaVersion) { 226 // From https://spec.openapis.org/oas/v3.0.3.html#properties: 227 // "Value MUST be a string. Multiple types via an array are not supported." 228 s.errf(n, `value of "type" must be a string in %v`, s.schemaVersion) 229 return 230 } 231 for i, _ := n.List(); i.Next(); { 232 set(i.Value()) 233 } 234 default: 235 s.errf(n, `value of "type" must be a string or list of strings`) 236 } 237 238 s.allowedTypes &= types 239 }