json_schemer 0.2.18 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +7 -7
- data/CHANGELOG.md +89 -0
- data/Gemfile.lock +35 -10
- data/README.md +402 -6
- data/bin/hostname_character_classes +42 -0
- data/bin/rake +29 -0
- data/exe/json_schemer +62 -0
- data/json_schemer.gemspec +9 -12
- data/lib/json_schemer/cached_resolver.rb +16 -0
- data/lib/json_schemer/configuration.rb +31 -0
- data/lib/json_schemer/content.rb +18 -0
- data/lib/json_schemer/draft201909/meta.rb +320 -0
- data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
- data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
- data/lib/json_schemer/draft201909/vocab.rb +31 -0
- data/lib/json_schemer/draft202012/meta.rb +364 -0
- data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
- data/lib/json_schemer/draft202012/vocab/content.rb +52 -0
- data/lib/json_schemer/draft202012/vocab/core.rb +160 -0
- data/lib/json_schemer/draft202012/vocab/format_annotation.rb +23 -0
- data/lib/json_schemer/draft202012/vocab/format_assertion.rb +23 -0
- data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
- data/lib/json_schemer/draft202012/vocab/unevaluated.rb +104 -0
- data/lib/json_schemer/draft202012/vocab/validation.rb +286 -0
- data/lib/json_schemer/draft202012/vocab.rb +105 -0
- data/lib/json_schemer/draft4/meta.rb +161 -0
- data/lib/json_schemer/draft4/vocab/validation.rb +39 -0
- data/lib/json_schemer/draft4/vocab.rb +18 -0
- data/lib/json_schemer/draft6/meta.rb +172 -0
- data/lib/json_schemer/draft6/vocab.rb +16 -0
- data/lib/json_schemer/draft7/meta.rb +183 -0
- data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
- data/lib/json_schemer/draft7/vocab.rb +30 -0
- data/lib/json_schemer/ecma_regexp.rb +51 -0
- data/lib/json_schemer/errors.rb +1 -0
- data/lib/json_schemer/format/duration.rb +23 -0
- data/lib/json_schemer/format/email.rb +56 -0
- data/lib/json_schemer/format/hostname.rb +58 -0
- data/lib/json_schemer/format/json_pointer.rb +18 -0
- data/lib/json_schemer/format/uri_template.rb +34 -0
- data/lib/json_schemer/format.rb +128 -109
- data/lib/json_schemer/keyword.rb +56 -0
- data/lib/json_schemer/location.rb +25 -0
- data/lib/json_schemer/openapi.rb +38 -0
- data/lib/json_schemer/openapi30/document.rb +1672 -0
- data/lib/json_schemer/openapi30/meta.rb +32 -0
- data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
- data/lib/json_schemer/openapi30/vocab.rb +12 -0
- data/lib/json_schemer/openapi31/document.rb +1557 -0
- data/lib/json_schemer/openapi31/meta.rb +136 -0
- data/lib/json_schemer/openapi31/vocab/base.rb +127 -0
- data/lib/json_schemer/openapi31/vocab.rb +18 -0
- data/lib/json_schemer/output.rb +56 -0
- data/lib/json_schemer/result.rb +242 -0
- data/lib/json_schemer/schema.rb +424 -0
- data/lib/json_schemer/version.rb +1 -1
- data/lib/json_schemer.rb +243 -29
- metadata +141 -25
- data/lib/json_schemer/cached_ref_resolver.rb +0 -14
- data/lib/json_schemer/schema/base.rb +0 -658
- data/lib/json_schemer/schema/draft4.rb +0 -44
- data/lib/json_schemer/schema/draft6.rb +0 -25
- data/lib/json_schemer/schema/draft7.rb +0 -32
@@ -0,0 +1,382 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module JSONSchemer
|
3
|
+
module Draft202012
|
4
|
+
module Vocab
|
5
|
+
module Applicator
|
6
|
+
class AllOf < Keyword
|
7
|
+
def error(formatted_instance_location:, **)
|
8
|
+
"value at #{formatted_instance_location} does not match all `allOf` schemas"
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse
|
12
|
+
value.map.with_index do |subschema, index|
|
13
|
+
subschema(subschema, index.to_s)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate(instance, instance_location, keyword_location, context)
|
18
|
+
nested = parsed.map.with_index do |subschema, index|
|
19
|
+
subschema.validate_instance(instance, instance_location, join_location(keyword_location, index.to_s), context)
|
20
|
+
end
|
21
|
+
result(instance, instance_location, keyword_location, nested.all?(&:valid), nested)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class AnyOf < Keyword
|
26
|
+
def error(formatted_instance_location:, **)
|
27
|
+
"value at #{formatted_instance_location} does not match any `anyOf` schemas"
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse
|
31
|
+
value.map.with_index do |subschema, index|
|
32
|
+
subschema(subschema, index.to_s)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate(instance, instance_location, keyword_location, context)
|
37
|
+
nested = parsed.map.with_index do |subschema, index|
|
38
|
+
subschema.validate_instance(instance, instance_location, join_location(keyword_location, index.to_s), context)
|
39
|
+
end
|
40
|
+
result(instance, instance_location, keyword_location, nested.any?(&:valid), nested)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class OneOf < Keyword
|
45
|
+
def error(formatted_instance_location:, **)
|
46
|
+
"value at #{formatted_instance_location} does not match exactly one `oneOf` schema"
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse
|
50
|
+
value.map.with_index do |subschema, index|
|
51
|
+
subschema(subschema, index.to_s)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate(instance, instance_location, keyword_location, context)
|
56
|
+
nested = parsed.map.with_index do |subschema, index|
|
57
|
+
subschema.validate_instance(instance, instance_location, join_location(keyword_location, index.to_s), context)
|
58
|
+
end
|
59
|
+
valid_count = nested.count(&:valid)
|
60
|
+
result(instance, instance_location, keyword_location, valid_count == 1, nested, :ignore_nested => valid_count > 1)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class Not < Keyword
|
65
|
+
def error(formatted_instance_location:, **)
|
66
|
+
"value at #{formatted_instance_location} matches `not` schema"
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse
|
70
|
+
subschema(value)
|
71
|
+
end
|
72
|
+
|
73
|
+
def validate(instance, instance_location, keyword_location, context)
|
74
|
+
subschema_result = parsed.validate_instance(instance, instance_location, keyword_location, context)
|
75
|
+
result(instance, instance_location, keyword_location, !subschema_result.valid, subschema_result.nested)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class If < Keyword
|
80
|
+
def parse
|
81
|
+
subschema(value)
|
82
|
+
end
|
83
|
+
|
84
|
+
def validate(instance, instance_location, keyword_location, context)
|
85
|
+
subschema_result = parsed.validate_instance(instance, instance_location, keyword_location, context)
|
86
|
+
result(instance, instance_location, keyword_location, true, subschema_result.nested, :annotation => subschema_result.valid)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class Then < Keyword
|
91
|
+
def error(formatted_instance_location:, **)
|
92
|
+
"value at #{formatted_instance_location} does not match conditional `then` schema"
|
93
|
+
end
|
94
|
+
|
95
|
+
def parse
|
96
|
+
subschema(value)
|
97
|
+
end
|
98
|
+
|
99
|
+
def validate(instance, instance_location, keyword_location, context)
|
100
|
+
return unless context.adjacent_results.key?(If) && context.adjacent_results.fetch(If).annotation
|
101
|
+
subschema_result = parsed.validate_instance(instance, instance_location, keyword_location, context)
|
102
|
+
result(instance, instance_location, keyword_location, subschema_result.valid, subschema_result.nested)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class Else < Keyword
|
107
|
+
def error(formatted_instance_location:, **)
|
108
|
+
"value at #{formatted_instance_location} does not match conditional `else` schema"
|
109
|
+
end
|
110
|
+
|
111
|
+
def parse
|
112
|
+
subschema(value)
|
113
|
+
end
|
114
|
+
|
115
|
+
def validate(instance, instance_location, keyword_location, context)
|
116
|
+
return unless context.adjacent_results.key?(If) && !context.adjacent_results.fetch(If).annotation
|
117
|
+
subschema_result = parsed.validate_instance(instance, instance_location, keyword_location, context)
|
118
|
+
result(instance, instance_location, keyword_location, subschema_result.valid, subschema_result.nested)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class DependentSchemas < Keyword
|
123
|
+
def error(formatted_instance_location:, **)
|
124
|
+
"value at #{formatted_instance_location} does not match applicable `dependentSchemas` schemas"
|
125
|
+
end
|
126
|
+
|
127
|
+
def parse
|
128
|
+
value.each_with_object({}) do |(key, subschema), out|
|
129
|
+
out[key] = subschema(subschema, key)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def validate(instance, instance_location, keyword_location, context)
|
134
|
+
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
|
135
|
+
|
136
|
+
nested = parsed.select do |key, _subschema|
|
137
|
+
instance.key?(key)
|
138
|
+
end.map do |key, subschema|
|
139
|
+
subschema.validate_instance(instance, instance_location, join_location(keyword_location, key), context)
|
140
|
+
end
|
141
|
+
|
142
|
+
result(instance, instance_location, keyword_location, nested.all?(&:valid), nested)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class PrefixItems < Keyword
|
147
|
+
def error(formatted_instance_location:, **)
|
148
|
+
"array items at #{formatted_instance_location} do not match corresponding `prefixItems` schemas"
|
149
|
+
end
|
150
|
+
|
151
|
+
def parse
|
152
|
+
value.map.with_index do |subschema, index|
|
153
|
+
subschema(subschema, index.to_s)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def validate(instance, instance_location, keyword_location, context)
|
158
|
+
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Array)
|
159
|
+
|
160
|
+
nested = instance.take(parsed.size).map.with_index do |item, index|
|
161
|
+
parsed.fetch(index).validate_instance(item, join_location(instance_location, index.to_s), join_location(keyword_location, index.to_s), context)
|
162
|
+
end
|
163
|
+
|
164
|
+
result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => (nested.size - 1))
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class Items < Keyword
|
169
|
+
def error(formatted_instance_location:, **)
|
170
|
+
"array items at #{formatted_instance_location} do not match `items` schema"
|
171
|
+
end
|
172
|
+
|
173
|
+
def parse
|
174
|
+
subschema(value)
|
175
|
+
end
|
176
|
+
|
177
|
+
def validate(instance, instance_location, keyword_location, context)
|
178
|
+
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Array)
|
179
|
+
|
180
|
+
evaluated_index = context.adjacent_results[PrefixItems]&.annotation
|
181
|
+
offset = evaluated_index ? (evaluated_index + 1) : 0
|
182
|
+
|
183
|
+
nested = instance.slice(offset..-1).map.with_index do |item, index|
|
184
|
+
parsed.validate_instance(item, join_location(instance_location, (offset + index).to_s), keyword_location, context)
|
185
|
+
end
|
186
|
+
|
187
|
+
result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => nested.any?)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
class Contains < Keyword
|
192
|
+
def error(formatted_instance_location:, **)
|
193
|
+
"array at #{formatted_instance_location} does not contain enough items that match `contains` schema"
|
194
|
+
end
|
195
|
+
|
196
|
+
def parse
|
197
|
+
subschema(value)
|
198
|
+
end
|
199
|
+
|
200
|
+
def validate(instance, instance_location, keyword_location, context)
|
201
|
+
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Array)
|
202
|
+
|
203
|
+
nested = instance.map.with_index do |item, index|
|
204
|
+
parsed.validate_instance(item, join_location(instance_location, index.to_s), keyword_location, context)
|
205
|
+
end
|
206
|
+
|
207
|
+
annotation = []
|
208
|
+
nested.each_with_index do |nested_result, index|
|
209
|
+
annotation << index if nested_result.valid
|
210
|
+
end
|
211
|
+
|
212
|
+
min_contains = schema.parsed['minContains']&.parsed || 1
|
213
|
+
|
214
|
+
result(instance, instance_location, keyword_location, annotation.size >= min_contains, nested, :annotation => annotation, :ignore_nested => true)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
class Properties < Keyword
|
219
|
+
def error(formatted_instance_location:, **)
|
220
|
+
"object properties at #{formatted_instance_location} do not match corresponding `properties` schemas"
|
221
|
+
end
|
222
|
+
|
223
|
+
def parse
|
224
|
+
value.each_with_object({}) do |(property, subschema), out|
|
225
|
+
out[property] = subschema(subschema, property)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def validate(instance, instance_location, keyword_location, context)
|
230
|
+
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
|
231
|
+
|
232
|
+
if root.before_property_validation.any?
|
233
|
+
original_instance = context.original_instance(instance_location)
|
234
|
+
root.before_property_validation.each do |hook|
|
235
|
+
parsed.each do |property, subschema|
|
236
|
+
hook.call(original_instance, property, subschema.value, schema.value)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
instance.replace(deep_stringify_keys(original_instance))
|
240
|
+
end
|
241
|
+
|
242
|
+
evaluated_keys = []
|
243
|
+
nested = []
|
244
|
+
|
245
|
+
parsed.each do |property, subschema|
|
246
|
+
if instance.key?(property)
|
247
|
+
evaluated_keys << property
|
248
|
+
nested << subschema.validate_instance(instance.fetch(property), join_location(instance_location, property), join_location(keyword_location, property), context)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
if root.after_property_validation.any?
|
253
|
+
original_instance = context.original_instance(instance_location)
|
254
|
+
root.after_property_validation.each do |hook|
|
255
|
+
parsed.each do |property, subschema|
|
256
|
+
hook.call(original_instance, property, subschema.value, schema.value)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
instance.replace(deep_stringify_keys(original_instance))
|
260
|
+
end
|
261
|
+
|
262
|
+
result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => evaluated_keys)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
class PatternProperties < Keyword
|
267
|
+
def error(formatted_instance_location:, **)
|
268
|
+
"object properties at #{formatted_instance_location} do not match corresponding `patternProperties` schemas"
|
269
|
+
end
|
270
|
+
|
271
|
+
def parse
|
272
|
+
value.each_with_object({}) do |(pattern, subschema), out|
|
273
|
+
out[pattern] = subschema(subschema, pattern)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def validate(instance, instance_location, keyword_location, context)
|
278
|
+
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
|
279
|
+
|
280
|
+
evaluated = Set[]
|
281
|
+
nested = []
|
282
|
+
|
283
|
+
parsed.each do |pattern, subschema|
|
284
|
+
regexp = root.resolve_regexp(pattern)
|
285
|
+
instance.each do |key, value|
|
286
|
+
if regexp.match?(key)
|
287
|
+
evaluated << key
|
288
|
+
nested << subschema.validate_instance(value, join_location(instance_location, key), join_location(keyword_location, pattern), context)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => evaluated.to_a)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
class AdditionalProperties < Keyword
|
298
|
+
def error(formatted_instance_location:, **)
|
299
|
+
"object properties at #{formatted_instance_location} do not match `additionalProperties` schema"
|
300
|
+
end
|
301
|
+
|
302
|
+
def false_schema_error(formatted_instance_location:, **)
|
303
|
+
"object property at #{formatted_instance_location} is a disallowed additional property"
|
304
|
+
end
|
305
|
+
|
306
|
+
def parse
|
307
|
+
subschema(value)
|
308
|
+
end
|
309
|
+
|
310
|
+
def validate(instance, instance_location, keyword_location, context)
|
311
|
+
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
|
312
|
+
|
313
|
+
evaluated_keys = context.adjacent_results[Properties]&.annotation || []
|
314
|
+
evaluated_keys += context.adjacent_results[PatternProperties]&.annotation || []
|
315
|
+
evaluated_keys = evaluated_keys.to_set
|
316
|
+
|
317
|
+
evaluated = instance.reject do |key, _value|
|
318
|
+
evaluated_keys.include?(key)
|
319
|
+
end
|
320
|
+
|
321
|
+
nested = evaluated.map do |key, value|
|
322
|
+
parsed.validate_instance(value, join_location(instance_location, key), keyword_location, context)
|
323
|
+
end
|
324
|
+
|
325
|
+
result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => evaluated.keys)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
class PropertyNames < Keyword
|
330
|
+
def error(formatted_instance_location:, **)
|
331
|
+
"object property names at #{formatted_instance_location} do not match `propertyNames` schema"
|
332
|
+
end
|
333
|
+
|
334
|
+
def parse
|
335
|
+
subschema(value)
|
336
|
+
end
|
337
|
+
|
338
|
+
def validate(instance, instance_location, keyword_location, context)
|
339
|
+
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
|
340
|
+
|
341
|
+
nested = instance.map do |key, _value|
|
342
|
+
parsed.validate_instance(key, instance_location, keyword_location, context)
|
343
|
+
end
|
344
|
+
|
345
|
+
result(instance, instance_location, keyword_location, nested.all?(&:valid), nested)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
class Dependencies < Keyword
|
350
|
+
def error(formatted_instance_location:, **)
|
351
|
+
"object at #{formatted_instance_location} either does not match applicable `dependencies` schemas or is missing required `dependencies` properties"
|
352
|
+
end
|
353
|
+
|
354
|
+
def parse
|
355
|
+
value.each_with_object({}) do |(key, value), out|
|
356
|
+
out[key] = value.is_a?(Array) ? value : subschema(value, key)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def validate(instance, instance_location, keyword_location, context)
|
361
|
+
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
|
362
|
+
|
363
|
+
existing_keys = instance.keys
|
364
|
+
|
365
|
+
nested = parsed.select do |key, _value|
|
366
|
+
instance.key?(key)
|
367
|
+
end.map do |key, value|
|
368
|
+
if value.is_a?(Array)
|
369
|
+
missing_keys = value - existing_keys
|
370
|
+
result(instance, instance_location, join_location(keyword_location, key), missing_keys.none?, :details => { 'missing_keys' => missing_keys })
|
371
|
+
else
|
372
|
+
value.validate_instance(instance, instance_location, join_location(keyword_location, key), context)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
result(instance, instance_location, keyword_location, nested.all?(&:valid), nested)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module JSONSchemer
|
3
|
+
module Draft202012
|
4
|
+
module Vocab
|
5
|
+
module Content
|
6
|
+
class ContentEncoding < Keyword
|
7
|
+
def parse
|
8
|
+
root.fetch_content_encoding(value) { raise UnknownContentEncoding, value }
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate(instance, instance_location, keyword_location, _context)
|
12
|
+
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String)
|
13
|
+
|
14
|
+
_valid, annotation = parsed.call(instance)
|
15
|
+
|
16
|
+
result(instance, instance_location, keyword_location, true, :annotation => annotation)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ContentMediaType < Keyword
|
21
|
+
def parse
|
22
|
+
root.fetch_content_media_type(value) { raise UnknownContentMediaType, value }
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate(instance, instance_location, keyword_location, context)
|
26
|
+
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String)
|
27
|
+
|
28
|
+
decoded_instance = context.adjacent_results[ContentEncoding]&.annotation || instance
|
29
|
+
_valid, annotation = parsed.call(decoded_instance)
|
30
|
+
|
31
|
+
result(instance, instance_location, keyword_location, true, :annotation => annotation)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class ContentSchema < Keyword
|
36
|
+
def parse
|
37
|
+
subschema(value)
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate(instance, instance_location, keyword_location, context)
|
41
|
+
return result(instance, instance_location, keyword_location, true) unless context.adjacent_results.key?(ContentMediaType)
|
42
|
+
|
43
|
+
parsed_instance = context.adjacent_results.fetch(ContentMediaType).annotation
|
44
|
+
annotation = parsed.validate_instance(parsed_instance, instance_location, keyword_location, context)
|
45
|
+
|
46
|
+
result(instance, instance_location, keyword_location, true, :annotation => annotation.to_output_unit)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module JSONSchemer
|
3
|
+
module Draft202012
|
4
|
+
module Vocab
|
5
|
+
module Core
|
6
|
+
class Schema < Keyword
|
7
|
+
def parse
|
8
|
+
schema.meta_schema = if value == schema.base_uri.to_s
|
9
|
+
schema
|
10
|
+
else
|
11
|
+
META_SCHEMAS_BY_BASE_URI_STR[value] || root.resolve_ref(URI(value))
|
12
|
+
end
|
13
|
+
value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Vocabulary < Keyword
|
18
|
+
def parse
|
19
|
+
value.each_with_object({}) do |(vocabulary, required), out|
|
20
|
+
if VOCABULARIES.key?(vocabulary)
|
21
|
+
out[vocabulary] = VOCABULARIES.fetch(vocabulary)
|
22
|
+
elsif required
|
23
|
+
raise UnknownVocabulary, vocabulary
|
24
|
+
end
|
25
|
+
end.tap do |vocabularies|
|
26
|
+
schema.keywords = vocabularies.sort_by do |vocabulary, _keywords|
|
27
|
+
VOCABULARY_ORDER.fetch(vocabulary, Float::INFINITY)
|
28
|
+
end.each_with_object({}) do |(_vocabulary, keywords), out|
|
29
|
+
out.merge!(keywords)
|
30
|
+
end
|
31
|
+
schema.keyword_order = schema.keywords.transform_values.with_index { |_keyword_class, index| index }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Id < Keyword
|
37
|
+
def parse
|
38
|
+
URI.join(schema.base_uri, value).tap do |uri|
|
39
|
+
schema.base_uri = uri
|
40
|
+
root.resources[:lexical][uri] = schema
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Anchor < Keyword
|
46
|
+
def parse
|
47
|
+
URI.join(schema.base_uri, "##{value}").tap do |uri|
|
48
|
+
root.resources[:lexical][uri] = schema
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Ref < Keyword
|
54
|
+
def self.exclusive?
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
def ref_uri
|
59
|
+
@ref_uri ||= URI.join(schema.base_uri, value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def ref_schema
|
63
|
+
@ref_schema ||= root.resolve_ref(ref_uri)
|
64
|
+
end
|
65
|
+
|
66
|
+
def validate(instance, instance_location, keyword_location, context)
|
67
|
+
ref_schema.validate_instance(instance, instance_location, keyword_location, context)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class DynamicAnchor < Keyword
|
72
|
+
def parse
|
73
|
+
URI.join(schema.base_uri, "##{value}").tap do |uri|
|
74
|
+
root.resources[:lexical][uri] = schema
|
75
|
+
root.resources[:dynamic][uri] = schema
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class DynamicRef < Keyword
|
81
|
+
def ref_uri
|
82
|
+
@ref_uri ||= URI.join(schema.base_uri, value)
|
83
|
+
end
|
84
|
+
|
85
|
+
def ref_schema
|
86
|
+
@ref_schema ||= root.resolve_ref(ref_uri)
|
87
|
+
end
|
88
|
+
|
89
|
+
def dynamic_anchor
|
90
|
+
return @dynamic_anchor if defined?(@dynamic_anchor)
|
91
|
+
fragment = ref_schema.parsed['$dynamicAnchor']&.parsed&.fragment
|
92
|
+
@dynamic_anchor = (fragment == ref_uri.fragment ? fragment : nil)
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate(instance, instance_location, keyword_location, context)
|
96
|
+
schema = ref_schema
|
97
|
+
|
98
|
+
if dynamic_anchor
|
99
|
+
context.dynamic_scope.each do |ancestor|
|
100
|
+
dynamic_uri = URI.join(ancestor.base_uri, "##{dynamic_anchor}")
|
101
|
+
if ancestor.root.resources.fetch(:dynamic).key?(dynamic_uri)
|
102
|
+
schema = ancestor.root.resources.fetch(:dynamic).fetch(dynamic_uri)
|
103
|
+
break
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
schema.validate_instance(instance, instance_location, keyword_location, context)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class Defs < Keyword
|
113
|
+
def parse
|
114
|
+
value.each_with_object({}) do |(key, subschema), out|
|
115
|
+
out[key] = subschema(subschema, key)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class Comment < Keyword; end
|
121
|
+
|
122
|
+
class XError < Keyword
|
123
|
+
def message(error_key)
|
124
|
+
value.is_a?(Hash) ? (value[error_key] || value[CATCHALL]) : value
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class UnknownKeyword < Keyword
|
129
|
+
def parse
|
130
|
+
if value.is_a?(Hash)
|
131
|
+
{}
|
132
|
+
elsif value.is_a?(Array)
|
133
|
+
[]
|
134
|
+
else
|
135
|
+
value
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def fetch(token)
|
140
|
+
if value.is_a?(Hash)
|
141
|
+
parsed[token] ||= JSONSchemer::Schema::UNKNOWN_KEYWORD_CLASS.new(value.fetch(token), self, token, schema)
|
142
|
+
elsif value.is_a?(Array)
|
143
|
+
parsed[token.to_i] ||= JSONSchemer::Schema::UNKNOWN_KEYWORD_CLASS.new(value.fetch(token.to_i), self, token, schema)
|
144
|
+
else
|
145
|
+
raise KeyError.new(:receiver => parsed, :key => token)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def parsed_schema
|
150
|
+
@parsed_schema ||= subschema(value)
|
151
|
+
end
|
152
|
+
|
153
|
+
def validate(instance, instance_location, keyword_location, _context)
|
154
|
+
result(instance, instance_location, keyword_location, true, :annotation => value)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module JSONSchemer
|
3
|
+
module Draft202012
|
4
|
+
module Vocab
|
5
|
+
module FormatAnnotation
|
6
|
+
class Format < Keyword
|
7
|
+
def error(formatted_instance_location:, **)
|
8
|
+
"value at #{formatted_instance_location} does not match format: #{value}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse
|
12
|
+
root.format && root.fetch_format(value, false)
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate(instance, instance_location, keyword_location, _context)
|
16
|
+
valid = parsed == false || parsed.call(instance, value)
|
17
|
+
result(instance, instance_location, keyword_location, valid, :annotation => value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module JSONSchemer
|
3
|
+
module Draft202012
|
4
|
+
module Vocab
|
5
|
+
module FormatAssertion
|
6
|
+
class Format < Keyword
|
7
|
+
def error(formatted_instance_location:, **)
|
8
|
+
"value at #{formatted_instance_location} does not match format: #{value}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse
|
12
|
+
root.format && root.fetch_format(value) { raise UnknownFormat, value }
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate(instance, instance_location, keyword_location, _context)
|
16
|
+
valid = parsed == false || parsed.call(instance, value)
|
17
|
+
result(instance, instance_location, keyword_location, valid, :annotation => value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module JSONSchemer
|
3
|
+
module Draft202012
|
4
|
+
module Vocab
|
5
|
+
module MetaData
|
6
|
+
class ReadOnly < Keyword
|
7
|
+
def error(formatted_instance_location:, **)
|
8
|
+
"value at #{formatted_instance_location} is `readOnly`"
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate(instance, instance_location, keyword_location, context)
|
12
|
+
valid = parsed != true || !context.access_mode || context.access_mode == 'read'
|
13
|
+
result(instance, instance_location, keyword_location, valid, :annotation => value)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class WriteOnly < Keyword
|
18
|
+
def error(formatted_instance_location:, **)
|
19
|
+
"value at #{formatted_instance_location} is `writeOnly`"
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate(instance, instance_location, keyword_location, context)
|
23
|
+
valid = parsed != true || !context.access_mode || context.access_mode == 'write'
|
24
|
+
result(instance, instance_location, keyword_location, valid, :annotation => value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|