github.com/icyphox/x@v0.0.355-0.20220311094250-029bd783e8b8/jsonschemax/keys.go (about) 1 package jsonschemax 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/sha256" 7 "encoding/json" 8 "fmt" 9 "math/big" 10 "regexp" 11 "sort" 12 "strings" 13 14 "github.com/pkg/errors" 15 16 "github.com/ory/jsonschema/v3" 17 18 "github.com/ory/x/stringslice" 19 ) 20 21 type ( 22 byName []Path 23 PathEnhancer interface { 24 EnhancePath(Path) map[string]interface{} 25 } 26 TypeHint int 27 ) 28 29 func (s byName) Len() int { return len(s) } 30 func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 31 func (s byName) Less(i, j int) bool { return s[i].Name < s[j].Name } 32 33 const ( 34 String TypeHint = iota + 1 35 Float 36 Int 37 Bool 38 JSON 39 Nil 40 41 BoolSlice 42 StringSlice 43 IntSlice 44 FloatSlice 45 ) 46 47 // Path represents a JSON Schema Path. 48 type Path struct { 49 // Title of the path. 50 Title string 51 52 // Description of the path. 53 Description string 54 55 // Examples of the path. 56 Examples []interface{} 57 58 // Name is the JSON path name. 59 Name string 60 61 // Default is the default value of that path. 62 Default interface{} 63 64 // Type is a prototype (e.g. float64(0)) of the path type. 65 Type interface{} 66 67 TypeHint 68 69 // Format is the format of the path if defined 70 Format string 71 72 // Pattern is the pattern of the path if defined 73 Pattern *regexp.Regexp 74 75 // Enum are the allowed enum values 76 Enum []interface{} 77 78 // first element in slice is constant value. note: slice is used to capture nil constant. 79 Constant []interface{} 80 81 // ReadOnly is whether the value is readonly 82 ReadOnly bool 83 84 // -1 if not specified 85 MinLength int 86 MaxLength int 87 88 // Required if set indicates this field is required. 89 Required bool 90 91 Minimum *big.Float 92 Maximum *big.Float 93 94 MultipleOf *big.Float 95 96 CustomProperties map[string]interface{} 97 } 98 99 // ListPathsBytes works like ListPathsWithRecursion but prepares the JSON Schema itself. 100 func ListPathsBytes(ctx context.Context, raw json.RawMessage, maxRecursion int16) ([]Path, error) { 101 compiler := jsonschema.NewCompiler() 102 compiler.ExtractAnnotations = true 103 id := fmt.Sprintf("%x.json", sha256.Sum256(raw)) 104 if err := compiler.AddResource(id, bytes.NewReader(raw)); err != nil { 105 return nil, err 106 } 107 compiler.ExtractAnnotations = true 108 return runPathsFromCompiler(ctx, id, compiler, maxRecursion, false) 109 } 110 111 // ListPathsWithRecursion will follow circular references until maxRecursion is reached, without 112 // returning an error. 113 func ListPathsWithRecursion(ctx context.Context, ref string, compiler *jsonschema.Compiler, maxRecursion uint8) ([]Path, error) { 114 return runPathsFromCompiler(ctx, ref, compiler, int16(maxRecursion), false) 115 } 116 117 // ListPaths lists all paths of a JSON Schema. Will return an error 118 // if circular references are found. 119 func ListPaths(ctx context.Context, ref string, compiler *jsonschema.Compiler) ([]Path, error) { 120 return runPathsFromCompiler(ctx, ref, compiler, -1, false) 121 } 122 123 // ListPathsWithArraysIncluded lists all paths of a JSON Schema. Will return an error 124 // if circular references are found. 125 // Includes arrays with `#`. 126 func ListPathsWithArraysIncluded(ctx context.Context, ref string, compiler *jsonschema.Compiler) ([]Path, error) { 127 return runPathsFromCompiler(ctx, ref, compiler, -1, true) 128 } 129 130 // ListPathsWithInitializedSchema loads the paths from the schema without compiling it. 131 // 132 // You MUST ensure that the compiler was using `ExtractAnnotations = true`. 133 func ListPathsWithInitializedSchema(schema *jsonschema.Schema) ([]Path, error) { 134 return runPaths(schema, -1, false) 135 } 136 137 // ListPathsWithInitializedSchemaAndArraysIncluded loads the paths from the schema without compiling it. 138 // 139 // You MUST ensure that the compiler was using `ExtractAnnotations = true`. 140 // Includes arrays with `#`. 141 func ListPathsWithInitializedSchemaAndArraysIncluded(schema *jsonschema.Schema) ([]Path, error) { 142 return runPaths(schema, -1, true) 143 } 144 145 func runPathsFromCompiler(ctx context.Context, ref string, compiler *jsonschema.Compiler, maxRecursion int16, includeArrays bool) ([]Path, error) { 146 if compiler == nil { 147 compiler = jsonschema.NewCompiler() 148 } 149 150 compiler.ExtractAnnotations = true 151 152 schema, err := compiler.Compile(ctx, ref) 153 if err != nil { 154 return nil, errors.WithStack(err) 155 } 156 157 return runPaths(schema, maxRecursion, includeArrays) 158 } 159 160 func runPaths(schema *jsonschema.Schema, maxRecursion int16, includeArrays bool) ([]Path, error) { 161 pointers := map[string]bool{} 162 paths, err := listPaths(schema, nil, nil, pointers, 0, maxRecursion, includeArrays) 163 if err != nil { 164 return nil, errors.WithStack(err) 165 } 166 167 sort.Stable(paths) 168 return makeUnique(paths) 169 } 170 171 func makeUnique(in byName) (byName, error) { 172 cache := make(map[string]Path) 173 for _, p := range in { 174 vc, ok := cache[p.Name] 175 if !ok { 176 cache[p.Name] = p 177 continue 178 } 179 180 if fmt.Sprintf("%T", p.Type) != fmt.Sprintf("%T", p.Type) { 181 return nil, errors.Errorf("multiple types %+v are not supported for path: %s", []interface{}{p.Type, vc.Type}, p.Name) 182 } 183 184 if vc.Default == nil { 185 cache[p.Name] = p 186 } 187 } 188 189 k := 0 190 out := make([]Path, len(cache)) 191 for _, v := range cache { 192 out[k] = v 193 k++ 194 } 195 196 paths := byName(out) 197 sort.Sort(paths) 198 return paths, nil 199 } 200 201 func appendPointer(in map[string]bool, pointer *jsonschema.Schema) map[string]bool { 202 out := make(map[string]bool) 203 for k, v := range in { 204 out[k] = v 205 } 206 out[fmt.Sprintf("%p", pointer)] = true 207 return out 208 } 209 210 func listPaths(schema *jsonschema.Schema, parent *jsonschema.Schema, parents []string, pointers map[string]bool, currentRecursion int16, maxRecursion int16, includeArrays bool) (byName, error) { 211 var pathType interface{} 212 var pathTypeHint TypeHint 213 var paths []Path 214 _, isCircular := pointers[fmt.Sprintf("%p", schema)] 215 216 if len(schema.Constant) > 0 { 217 switch schema.Constant[0].(type) { 218 case float64, json.Number: 219 pathType = float64(0) 220 pathTypeHint = Float 221 case int8, int16, int, int64: 222 pathType = int64(0) 223 pathTypeHint = Int 224 case string: 225 pathType = "" 226 pathTypeHint = String 227 case bool: 228 pathType = false 229 pathTypeHint = Bool 230 default: 231 pathType = schema.Constant[0] 232 pathTypeHint = JSON 233 } 234 } else if len(schema.Types) == 1 { 235 switch schema.Types[0] { 236 case "null": 237 pathType = nil 238 pathTypeHint = Nil 239 case "boolean": 240 pathType = false 241 pathTypeHint = Bool 242 case "number": 243 pathType = float64(0) 244 pathTypeHint = Float 245 case "integer": 246 pathType = float64(0) 247 pathTypeHint = Int 248 case "string": 249 pathType = "" 250 pathTypeHint = String 251 case "array": 252 pathType = []interface{}{} 253 if schema.Items != nil { 254 var itemSchemas []*jsonschema.Schema 255 switch t := schema.Items.(type) { 256 case []*jsonschema.Schema: 257 itemSchemas = t 258 case *jsonschema.Schema: 259 itemSchemas = []*jsonschema.Schema{t} 260 } 261 var types []string 262 for _, is := range itemSchemas { 263 types = append(types, is.Types...) 264 if is.Ref != nil { 265 types = append(types, is.Ref.Types...) 266 } 267 } 268 types = stringslice.Unique(types) 269 if len(types) == 1 { 270 switch types[0] { 271 case "boolean": 272 pathType = []bool{} 273 pathTypeHint = BoolSlice 274 case "number": 275 pathType = []float64{} 276 pathTypeHint = FloatSlice 277 case "integer": 278 pathType = []float64{} 279 pathTypeHint = IntSlice 280 case "string": 281 pathType = []string{} 282 pathTypeHint = StringSlice 283 default: 284 pathType = []interface{}{} 285 pathTypeHint = JSON 286 } 287 } 288 } 289 case "object": 290 pathType = map[string]interface{}{} 291 pathTypeHint = JSON 292 } 293 } else if len(schema.Types) > 2 { 294 pathType = nil 295 pathTypeHint = JSON 296 } 297 298 var def interface{} = schema.Default 299 if v, ok := def.(json.Number); ok { 300 def, _ = v.Float64() 301 } 302 303 if (pathType != nil || schema.Default != nil) && len(parents) > 0 { 304 name := parents[len(parents)-1] 305 var required bool 306 if parent != nil { 307 for _, r := range parent.Required { 308 if r == name { 309 required = true 310 break 311 } 312 } 313 } 314 315 path := Path{ 316 Name: strings.Join(parents, "."), 317 Default: def, 318 Type: pathType, 319 TypeHint: pathTypeHint, 320 Format: schema.Format, 321 Pattern: schema.Pattern, 322 Enum: schema.Enum, 323 Constant: schema.Constant, 324 MinLength: schema.MinLength, 325 MaxLength: schema.MaxLength, 326 Minimum: schema.Minimum, 327 Maximum: schema.Maximum, 328 MultipleOf: schema.MultipleOf, 329 ReadOnly: schema.ReadOnly, 330 Title: schema.Title, 331 Description: schema.Description, 332 Examples: schema.Examples, 333 Required: required, 334 } 335 336 for _, e := range schema.Extensions { 337 if enhancer, ok := e.(PathEnhancer); ok { 338 path.CustomProperties = enhancer.EnhancePath(path) 339 } 340 } 341 paths = append(paths, path) 342 } 343 344 if isCircular { 345 if maxRecursion == -1 { 346 return nil, errors.Errorf("detected circular dependency in schema path: %s", strings.Join(parents, ".")) 347 } else if currentRecursion > maxRecursion { 348 return paths, nil 349 } 350 currentRecursion++ 351 } 352 353 if schema.Ref != nil { 354 path, err := listPaths(schema.Ref, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) 355 if err != nil { 356 return nil, err 357 } 358 paths = append(paths, path...) 359 } 360 361 if schema.Not != nil { 362 path, err := listPaths(schema.Not, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) 363 if err != nil { 364 return nil, err 365 } 366 paths = append(paths, path...) 367 } 368 369 if schema.If != nil { 370 path, err := listPaths(schema.If, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) 371 if err != nil { 372 return nil, err 373 } 374 paths = append(paths, path...) 375 } 376 377 if schema.Then != nil { 378 path, err := listPaths(schema.Then, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) 379 if err != nil { 380 return nil, err 381 } 382 paths = append(paths, path...) 383 } 384 385 if schema.Else != nil { 386 path, err := listPaths(schema.Else, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) 387 if err != nil { 388 return nil, err 389 } 390 paths = append(paths, path...) 391 } 392 393 for _, sub := range schema.AllOf { 394 path, err := listPaths(sub, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) 395 if err != nil { 396 return nil, err 397 } 398 paths = append(paths, path...) 399 } 400 401 for _, sub := range schema.AnyOf { 402 path, err := listPaths(sub, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) 403 if err != nil { 404 return nil, err 405 } 406 paths = append(paths, path...) 407 } 408 409 for _, sub := range schema.OneOf { 410 path, err := listPaths(sub, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) 411 if err != nil { 412 return nil, err 413 } 414 paths = append(paths, path...) 415 } 416 417 for name, sub := range schema.Properties { 418 path, err := listPaths(sub, schema, append(parents, name), appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) 419 if err != nil { 420 return nil, err 421 } 422 paths = append(paths, path...) 423 } 424 425 if schema.Items != nil && includeArrays { 426 switch t := schema.Items.(type) { 427 case []*jsonschema.Schema: 428 for _, sub := range t { 429 path, err := listPaths(sub, schema, append(parents, "#"), appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) 430 if err != nil { 431 return nil, err 432 } 433 paths = append(paths, path...) 434 } 435 case *jsonschema.Schema: 436 path, err := listPaths(t, schema, append(parents, "#"), appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) 437 if err != nil { 438 return nil, err 439 } 440 paths = append(paths, path...) 441 } 442 } 443 444 return paths, nil 445 }