1 package jsonschemax
2
3 import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/json"
7 "fmt"
8 "math/big"
9 "regexp"
10 "sort"
11 "strings"
12
13 "github.com/pkg/errors"
14
15 "github.com/ory/jsonschema/v3"
16
17 "github.com/ory/x/stringslice"
18 )
19
20 type (
21 byName []Path
22 PathEnhancer interface {
23 EnhancePath(Path) map[string]interface{}
24 }
25 TypeHint int
26 )
27
28 func (s byName) Len() int { return len(s) }
29 func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
30 func (s byName) Less(i, j int) bool { return s[i].Name < s[j].Name }
31
32 const (
33 String TypeHint = iota + 1
34 Float
35 Int
36 Bool
37 JSON
38 Nil
39
40 BoolSlice
41 StringSlice
42 IntSlice
43 FloatSlice
44 )
45
46
47 type Path struct {
48
49 Name string
50
51
52 Default interface{}
53
54
55 Type interface{}
56
57 TypeHint
58
59
60 Format string
61
62
63 Pattern *regexp.Regexp
64
65
66 Enum []interface{}
67
68
69 Constant []interface{}
70
71
72 ReadOnly bool
73
74
75 MinLength int
76 MaxLength int
77
78 Minimum *big.Float
79 Maximum *big.Float
80
81 MultipleOf *big.Float
82
83 CustomProperties map[string]interface{}
84 }
85
86
87 func ListPathsBytes(raw json.RawMessage, maxRecursion int16) ([]Path, error) {
88 compiler := jsonschema.NewCompiler()
89 compiler.ExtractAnnotations = true
90 id := fmt.Sprintf("%x.json", sha256.Sum256(raw))
91 if err := compiler.AddResource(id, bytes.NewReader(raw)); err != nil {
92 return nil, err
93 }
94 compiler.ExtractAnnotations = true
95 return runPaths(id, compiler, maxRecursion)
96 }
97
98
99
100 func ListPathsWithRecursion(ref string, compiler *jsonschema.Compiler, maxRecursion uint8) ([]Path, error) {
101 return runPaths(ref, compiler, int16(maxRecursion))
102 }
103
104
105
106 func ListPaths(ref string, compiler *jsonschema.Compiler) ([]Path, error) {
107 return runPaths(ref, compiler, -1)
108 }
109
110 func runPaths(ref string, compiler *jsonschema.Compiler, maxRecursion int16) ([]Path, error) {
111 if compiler == nil {
112 compiler = jsonschema.NewCompiler()
113 }
114
115 compiler.ExtractAnnotations = true
116 pointers := map[string]bool{}
117
118 schema, err := compiler.Compile(ref)
119 if err != nil {
120 return nil, errors.WithStack(err)
121 }
122
123 paths, err := listPaths(schema, nil, pointers, 0, maxRecursion)
124 if err != nil {
125 return nil, errors.WithStack(err)
126 }
127
128 sort.Sort(paths)
129 return makeUnique(paths)
130 }
131
132 func makeUnique(in byName) (byName, error) {
133 cache := make(map[string]Path)
134 for _, p := range in {
135 vc, ok := cache[p.Name]
136 if !ok {
137 cache[p.Name] = p
138 continue
139 }
140
141 if fmt.Sprintf("%T", p.Type) != fmt.Sprintf("%T", p.Type) {
142 return nil, errors.Errorf("multiple types %+v are not supported for path: %s", []interface{}{p.Type, vc.Type}, p.Name)
143 }
144
145 if vc.Default == nil {
146 cache[p.Name] = p
147 }
148 }
149
150 k := 0
151 out := make([]Path, len(cache))
152 for _, v := range cache {
153 out[k] = v
154 k++
155 }
156
157 paths := byName(out)
158 sort.Sort(paths)
159 return paths, nil
160 }
161
162 func appendPointer(in map[string]bool, pointer *jsonschema.Schema) map[string]bool {
163 out := make(map[string]bool)
164 for k, v := range in {
165 out[k] = v
166 }
167 out[fmt.Sprintf("%p", pointer)] = true
168 return out
169 }
170
171 func listPaths(schema *jsonschema.Schema, parents []string, pointers map[string]bool, currentRecursion int16, maxRecursion int16) (byName, error) {
172 var pathType interface{}
173 var pathTypeHint TypeHint
174 var paths []Path
175 _, isCircular := pointers[fmt.Sprintf("%p", schema)]
176
177 if len(schema.Constant) > 0 {
178 switch schema.Constant[0].(type) {
179 case float64, json.Number:
180 pathType = float64(0)
181 pathTypeHint = Float
182 case int8, int16, int, int64:
183 pathType = int64(0)
184 pathTypeHint = Int
185 case string:
186 pathType = ""
187 pathTypeHint = String
188 case bool:
189 pathType = false
190 pathTypeHint = Bool
191 default:
192 pathType = schema.Constant[0]
193 pathTypeHint = JSON
194 }
195 } else if len(schema.Types) == 1 {
196 switch schema.Types[0] {
197 case "null":
198 pathType = nil
199 pathTypeHint = Nil
200 case "boolean":
201 pathType = false
202 pathTypeHint = Bool
203 case "number":
204 pathType = float64(0)
205 pathTypeHint = Float
206 case "integer":
207 pathType = float64(0)
208 pathTypeHint = Int
209 case "string":
210 pathType = ""
211 pathTypeHint = String
212 case "array":
213 pathType = []interface{}{}
214 if schema.Items != nil {
215 var types []string
216 switch t := schema.Items.(type) {
217 case []*jsonschema.Schema:
218 for _, tt := range t {
219 types = append(types, tt.Types...)
220 }
221 case *jsonschema.Schema:
222 types = append(types, t.Types...)
223 }
224 types = stringslice.Unique(types)
225 if len(types) == 1 {
226 switch types[0] {
227 case "boolean":
228 pathType = []bool{}
229 pathTypeHint = BoolSlice
230 case "number":
231 pathType = []float64{}
232 pathTypeHint = FloatSlice
233 case "integer":
234 pathType = []float64{}
235 pathTypeHint = IntSlice
236 case "string":
237 pathType = []string{}
238 pathTypeHint = StringSlice
239 }
240 }
241 }
242 case "object":
243
244 if len(schema.Properties) == 0 {
245 pathType = map[string]interface{}{}
246 }
247 pathTypeHint = JSON
248 }
249 } else if len(schema.Types) > 2 {
250 pathType = nil
251 pathTypeHint = JSON
252 }
253
254 var def interface{} = schema.Default
255 if v, ok := def.(json.Number); ok {
256 def, _ = v.Float64()
257 }
258 if (pathType != nil || schema.Default != nil) && len(parents) > 0 {
259 path := Path{
260 Name: strings.Join(parents, "."),
261 Default: def,
262 Type: pathType,
263 TypeHint: pathTypeHint,
264 Format: schema.Format,
265 Pattern: schema.Pattern,
266 Enum: schema.Enum,
267 Constant: schema.Constant,
268 MinLength: schema.MinLength,
269 MaxLength: schema.MaxLength,
270 Minimum: schema.Minimum,
271 Maximum: schema.Maximum,
272 MultipleOf: schema.MultipleOf,
273 ReadOnly: schema.ReadOnly,
274 }
275 for _, e := range schema.Extensions {
276 if enhancer, ok := e.(PathEnhancer); ok {
277 path.CustomProperties = enhancer.EnhancePath(path)
278 }
279 }
280 paths = append(paths, path)
281 }
282
283 if isCircular {
284 if maxRecursion == -1 {
285 return nil, errors.Errorf("detected circular dependency in schema path: %s", strings.Join(parents, "."))
286 } else if currentRecursion > maxRecursion {
287 return paths, nil
288 }
289 currentRecursion++
290 }
291
292 if schema.Ref != nil {
293 path, err := listPaths(schema.Ref, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
294 if err != nil {
295 return nil, err
296 }
297 paths = append(paths, path...)
298 }
299
300 if schema.Not != nil {
301 path, err := listPaths(schema.Not, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
302 if err != nil {
303 return nil, err
304 }
305 paths = append(paths, path...)
306 }
307
308 if schema.If != nil {
309 path, err := listPaths(schema.If, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
310 if err != nil {
311 return nil, err
312 }
313 paths = append(paths, path...)
314 }
315
316 if schema.Then != nil {
317 path, err := listPaths(schema.Then, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
318 if err != nil {
319 return nil, err
320 }
321 paths = append(paths, path...)
322 }
323
324 if schema.Else != nil {
325 path, err := listPaths(schema.Else, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
326 if err != nil {
327 return nil, err
328 }
329 paths = append(paths, path...)
330 }
331
332 for _, sub := range schema.AllOf {
333 path, err := listPaths(sub, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
334 if err != nil {
335 return nil, err
336 }
337 paths = append(paths, path...)
338 }
339
340 for _, sub := range schema.AnyOf {
341 path, err := listPaths(sub, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
342 if err != nil {
343 return nil, err
344 }
345 paths = append(paths, path...)
346 }
347
348 for _, sub := range schema.OneOf {
349 path, err := listPaths(sub, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
350 if err != nil {
351 return nil, err
352 }
353 paths = append(paths, path...)
354 }
355
356 for name, sub := range schema.Properties {
357 path, err := listPaths(sub, append(parents, name), appendPointer(pointers, schema), currentRecursion, maxRecursion)
358 if err != nil {
359 return nil, err
360 }
361 paths = append(paths, path...)
362 }
363
364 return paths, nil
365 }
366
View as plain text