cuelang.org/go@v0.13.0/encoding/jsonschema/constraints_combinator.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 "strconv" 19 20 "cuelang.org/go/cue" 21 "cuelang.org/go/cue/ast" 22 "cuelang.org/go/cue/token" 23 ) 24 25 // Constraint combinators. 26 27 func constraintAllOf(key string, n cue.Value, s *state) { 28 var knownTypes cue.Kind 29 items := s.listItems("allOf", n, false) 30 if len(items) == 0 { 31 s.errf(n, "allOf requires at least one subschema") 32 return 33 } 34 a := make([]ast.Expr, 0, len(items)) 35 for _, v := range items { 36 x, sub := s.schemaState(v, s.allowedTypes, nil) 37 s.allowedTypes &= sub.allowedTypes 38 if sub.hasConstraints { 39 // This might seem a little odd, since the actual 40 // types are the intersection of the known types 41 // of the allOf members. However, knownTypes 42 // is really there to avoid adding redundant disjunctions. 43 // So if we have (int & string) & (disjunction) 44 // we definitely don't have to add int or string to 45 // disjunction. 46 knownTypes |= sub.knownTypes 47 a = append(a, x) 48 } 49 } 50 // TODO maybe give an error/warning if s.allowedTypes == 0 51 // as that's a known-impossible assertion? 52 if len(a) > 0 { 53 s.knownTypes &= knownTypes 54 if len(a) == 1 { 55 // Only one possibility. Use that. 56 s.all.add(n, a[0]) 57 return 58 } 59 s.all.add(n, ast.NewCall( 60 ast.NewIdent("matchN"), 61 // TODO it would be nice to be able to use a special sentinel "all" value 62 // here rather than redundantly encoding the length of the list. 63 &ast.BasicLit{ 64 Kind: token.INT, 65 Value: strconv.Itoa(len(items)), 66 }, 67 ast.NewList(a...), 68 )) 69 } 70 } 71 72 func constraintAnyOf(key string, n cue.Value, s *state) { 73 var types cue.Kind 74 var knownTypes cue.Kind 75 items := s.listItems("anyOf", n, false) 76 if len(items) == 0 { 77 s.errf(n, "anyOf requires at least one subschema") 78 return 79 } 80 a := make([]ast.Expr, 0, len(items)) 81 for _, v := range items { 82 x, sub := s.schemaState(v, s.allowedTypes, nil) 83 if sub.allowedTypes == 0 { 84 // Nothing is allowed; omit. 85 continue 86 } 87 types |= sub.allowedTypes 88 knownTypes |= sub.knownTypes 89 a = append(a, x) 90 } 91 if len(a) == 0 { 92 // Nothing at all is allowed. 93 s.allowedTypes = 0 94 return 95 } 96 if len(a) == 1 { 97 s.all.add(n, a[0]) 98 return 99 } 100 s.allowedTypes &= types 101 s.knownTypes &= knownTypes 102 s.all.add(n, ast.NewCall( 103 ast.NewIdent("matchN"), 104 &ast.UnaryExpr{ 105 Op: token.GEQ, 106 X: &ast.BasicLit{ 107 Kind: token.INT, 108 Value: "1", 109 }, 110 }, 111 ast.NewList(a...), 112 )) 113 } 114 115 func constraintOneOf(key string, n cue.Value, s *state) { 116 var types cue.Kind 117 var knownTypes cue.Kind 118 needsConstraint := false 119 items := s.listItems("oneOf", n, false) 120 if len(items) == 0 { 121 s.errf(n, "oneOf requires at least one subschema") 122 return 123 } 124 a := make([]ast.Expr, 0, len(items)) 125 for _, v := range items { 126 x, sub := s.schemaState(v, s.allowedTypes, nil) 127 if sub.allowedTypes == 0 { 128 // Nothing is allowed; omit 129 continue 130 } 131 132 // TODO: make more finegrained by making it two pass. 133 if sub.hasConstraints { 134 needsConstraint = true 135 } else if (types & sub.allowedTypes) != 0 { 136 // If there's overlap between the unconstrained elements, 137 // we'll definitely need to add a constraint. 138 needsConstraint = true 139 } 140 types |= sub.allowedTypes 141 knownTypes |= sub.knownTypes 142 a = append(a, x) 143 } 144 // TODO if there are no elements in the oneOf, validation 145 // should fail. 146 s.allowedTypes &= types 147 if len(a) > 0 && needsConstraint { 148 s.knownTypes &= knownTypes 149 if len(a) == 1 { 150 // Only one possibility. Use that. 151 s.all.add(n, a[0]) 152 return 153 } 154 s.all.add(n, ast.NewCall( 155 ast.NewIdent("matchN"), 156 &ast.BasicLit{ 157 Kind: token.INT, 158 Value: "1", 159 }, 160 ast.NewList(a...), 161 )) 162 } 163 164 // TODO: oneOf({a:x}, {b:y}, ..., not(anyOf({a:x}, {b:y}, ...))), 165 // can be translated to {} | {a:x}, {b:y}, ... 166 } 167 168 func constraintNot(key string, n cue.Value, s *state) { 169 subSchema := s.schema(n) 170 s.all.add(n, ast.NewCall( 171 ast.NewIdent("matchN"), 172 &ast.BasicLit{ 173 Kind: token.INT, 174 Value: "0", 175 }, 176 ast.NewList(subSchema), 177 )) 178 } 179 180 func constraintIf(key string, n cue.Value, s *state) { 181 s.ifConstraint = n 182 } 183 184 func constraintThen(key string, n cue.Value, s *state) { 185 s.thenConstraint = n 186 } 187 188 func constraintElse(key string, n cue.Value, s *state) { 189 s.elseConstraint = n 190 } 191 192 // constraintIfThenElse is not implemented as a standard constraint 193 // function because it needs to operate knowing about the presence 194 // of all of "if", "then" and "else". 195 func constraintIfThenElse(s *state) { 196 hasIf, hasThen, hasElse := s.ifConstraint.Exists(), s.thenConstraint.Exists(), s.elseConstraint.Exists() 197 if !hasIf || (!hasThen && !hasElse) { 198 return 199 } 200 var ifExpr, thenExpr, elseExpr ast.Expr 201 ifExpr, ifSub := s.schemaState(s.ifConstraint, s.allowedTypes, nil) 202 if hasThen { 203 // The allowed types of the "then" constraint are constrained both 204 // by the current constraints and the "if" constraint. 205 thenExpr, _ = s.schemaState(s.thenConstraint, s.allowedTypes&ifSub.allowedTypes, nil) 206 } 207 if hasElse { 208 elseExpr, _ = s.schemaState(s.elseConstraint, s.allowedTypes, nil) 209 } 210 if thenExpr == nil { 211 thenExpr = top() 212 } 213 if elseExpr == nil { 214 elseExpr = top() 215 } 216 s.all.add(s.pos, ast.NewCall( 217 ast.NewIdent("matchIf"), 218 ifExpr, 219 thenExpr, 220 elseExpr, 221 )) 222 }