schemurai 1.0.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +206 -0
- data/lib/schemurai/dialect.rb +118 -0
- data/lib/schemurai/dialect_keywords.rb +91 -0
- data/lib/schemurai/dialects/draft2019_09.rb +27 -0
- data/lib/schemurai/dialects/draft2020_12.rb +27 -0
- data/lib/schemurai/dialects/draft7.rb +27 -0
- data/lib/schemurai/evaluation.rb +46 -0
- data/lib/schemurai/evaluator.rb +857 -0
- data/lib/schemurai/formats.rb +403 -0
- data/lib/schemurai/meta_schemas/draft2019_09.rb +48 -0
- data/lib/schemurai/meta_schemas/draft2020_12.rb +48 -0
- data/lib/schemurai/meta_schemas/draft7.rb +121 -0
- data/lib/schemurai/meta_schemas.rb +28 -0
- data/lib/schemurai/schema_graph.rb +481 -0
- data/lib/schemurai/schema_node.rb +55 -0
- data/lib/schemurai/version.rb +5 -0
- data/lib/schemurai.rb +110 -0
- metadata +76 -0
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "base64"
|
|
5
|
+
require_relative "evaluation"
|
|
6
|
+
|
|
7
|
+
module Schemurai
|
|
8
|
+
module Internal
|
|
9
|
+
class Evaluator
|
|
10
|
+
MISSING_SEGMENT = Object.new.freeze
|
|
11
|
+
|
|
12
|
+
def initialize(graph, root, content: false, format: false)
|
|
13
|
+
@validate_content = content
|
|
14
|
+
@validate_format = format
|
|
15
|
+
@graph = graph
|
|
16
|
+
@root = root
|
|
17
|
+
@regexps = nil
|
|
18
|
+
@active = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate(instance)
|
|
22
|
+
errors = []
|
|
23
|
+
each_error(instance) { |error| errors << error }
|
|
24
|
+
Result.new(errors)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def valid?(instance)
|
|
28
|
+
@error_callback = nil
|
|
29
|
+
@error_count = 0
|
|
30
|
+
@track_dynamic_scope = @graph.dynamic_scope?
|
|
31
|
+
@instance_path = nil
|
|
32
|
+
@schema_path = nil
|
|
33
|
+
evaluate_valid(@root, instance)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private def each_error(instance, &callback)
|
|
37
|
+
@error_callback = callback
|
|
38
|
+
@error_count = 0
|
|
39
|
+
@track_dynamic_scope = @graph.dynamic_scope?
|
|
40
|
+
@instance_path = []
|
|
41
|
+
@schema_path = []
|
|
42
|
+
evaluate(@root, instance)
|
|
43
|
+
ensure
|
|
44
|
+
@error_callback = nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private def evaluate_valid(node, instance)
|
|
48
|
+
schema = node.schema
|
|
49
|
+
return schema if schema == true || schema == false
|
|
50
|
+
return true unless schema.is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
# These applicators depend on annotations collected from sibling
|
|
53
|
+
# applicators. Keep them on the full evaluation path; schemas without
|
|
54
|
+
# them can avoid allocating Evaluation objects and JSON Pointer paths.
|
|
55
|
+
if schema.key?("unevaluatedProperties") || schema.key?("unevaluatedItems")
|
|
56
|
+
@instance_path ||= []
|
|
57
|
+
@schema_path ||= []
|
|
58
|
+
return evaluate(node, instance).valid?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if @track_dynamic_scope
|
|
62
|
+
entered_scope = @dynamic_scope.nil? || !@dynamic_scope.last.equal?(node.resource)
|
|
63
|
+
(@dynamic_scope ||= []) << node.resource if entered_scope
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if schema.key?("$ref")
|
|
67
|
+
return false unless valid_reference?(node, @graph.resolve(node, schema["$ref"]), instance)
|
|
68
|
+
return true unless node.dialect.ref_siblings?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if schema.key?("$recursiveRef")
|
|
72
|
+
return false unless valid_reference?(node, recursive_target(node, schema["$recursiveRef"]), instance)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if schema.key?("$dynamicRef")
|
|
76
|
+
return false unless valid_reference?(node, dynamic_target(node, schema["$dynamicRef"]), instance)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
keywords = node.keyword_mask
|
|
80
|
+
if keywords.zero? && format_asserted?(node)
|
|
81
|
+
return !instance.is_a?(String) || node.format.call(instance)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
categories = Internal::Dialect
|
|
85
|
+
return false if (keywords & categories::TYPE) != 0 && !valid_type?(schema, instance)
|
|
86
|
+
return false if (keywords & categories::ENUM) != 0 && !valid_enum?(schema, instance)
|
|
87
|
+
return false if (keywords & categories::COMBINER) != 0 && !valid_combiners?(node, instance)
|
|
88
|
+
|
|
89
|
+
case instance
|
|
90
|
+
when Hash
|
|
91
|
+
(keywords & categories::OBJECT) == 0 || valid_object?(node, instance)
|
|
92
|
+
when Array
|
|
93
|
+
(keywords & categories::ARRAY) == 0 || valid_array?(node, instance)
|
|
94
|
+
when String
|
|
95
|
+
if (keywords & categories::STRING) != 0
|
|
96
|
+
valid_string?(node, instance)
|
|
97
|
+
elsif format_asserted?(node)
|
|
98
|
+
node.format.call(instance)
|
|
99
|
+
else
|
|
100
|
+
true
|
|
101
|
+
end
|
|
102
|
+
when Numeric
|
|
103
|
+
instance.is_a?(Complex) || (keywords & categories::NUMBER) == 0 || valid_number?(schema, instance)
|
|
104
|
+
else
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
rescue ResolutionError
|
|
108
|
+
false
|
|
109
|
+
ensure
|
|
110
|
+
@dynamic_scope.pop if entered_scope
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private def valid_reference?(source, target, instance)
|
|
114
|
+
instances = active_instances(source)
|
|
115
|
+
instance_id = instance.object_id
|
|
116
|
+
return true if instances[instance_id]
|
|
117
|
+
|
|
118
|
+
instances[instance_id] = true
|
|
119
|
+
activated = true
|
|
120
|
+
evaluate_valid(target, instance)
|
|
121
|
+
ensure
|
|
122
|
+
instances&.delete(instance_id) if activated
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private def valid_type?(schema, value)
|
|
126
|
+
types = schema["type"]
|
|
127
|
+
return types.any? { |type| type?(value, type) } if types.is_a?(Array)
|
|
128
|
+
|
|
129
|
+
type?(value, types)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private def valid_enum?(schema, value)
|
|
133
|
+
return false if schema.key?("enum") && !schema["enum"].any? { |candidate| json_equal?(candidate, value) }
|
|
134
|
+
return false if schema.key?("const") && !json_equal?(schema["const"], value)
|
|
135
|
+
|
|
136
|
+
true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private def valid_combiners?(node, value)
|
|
140
|
+
schema = node.schema
|
|
141
|
+
if schema.key?("allOf")
|
|
142
|
+
schema["allOf"].each_index do |index|
|
|
143
|
+
return false unless evaluate_valid(node.child("allOf", index), value)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
if schema.key?("anyOf")
|
|
147
|
+
matched = schema["anyOf"].each_index.any? do |index|
|
|
148
|
+
evaluate_valid(node.child("anyOf", index), value)
|
|
149
|
+
end
|
|
150
|
+
return false unless matched
|
|
151
|
+
end
|
|
152
|
+
if schema.key?("oneOf")
|
|
153
|
+
matches = 0
|
|
154
|
+
schema["oneOf"].each_index do |index|
|
|
155
|
+
matches += 1 if evaluate_valid(node.child("oneOf", index), value)
|
|
156
|
+
return false if matches > 1
|
|
157
|
+
end
|
|
158
|
+
return false unless matches == 1
|
|
159
|
+
end
|
|
160
|
+
return false if schema.key?("not") && evaluate_valid(node.child("not"), value)
|
|
161
|
+
|
|
162
|
+
if schema.key?("if")
|
|
163
|
+
branch = evaluate_valid(node.child("if"), value) ? "then" : "else"
|
|
164
|
+
return false if schema.key?(branch) && !evaluate_valid(node.child(branch), value)
|
|
165
|
+
end
|
|
166
|
+
true
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private def valid_number?(schema, value)
|
|
170
|
+
actual = nil
|
|
171
|
+
if schema.key?("maximum")
|
|
172
|
+
actual ||= decimal(value)
|
|
173
|
+
return false unless actual <= decimal(schema["maximum"])
|
|
174
|
+
end
|
|
175
|
+
if schema.key?("minimum")
|
|
176
|
+
actual ||= decimal(value)
|
|
177
|
+
return false unless actual >= decimal(schema["minimum"])
|
|
178
|
+
end
|
|
179
|
+
if schema.key?("exclusiveMaximum")
|
|
180
|
+
actual ||= decimal(value)
|
|
181
|
+
return false unless actual < decimal(schema["exclusiveMaximum"])
|
|
182
|
+
end
|
|
183
|
+
if schema.key?("exclusiveMinimum")
|
|
184
|
+
actual ||= decimal(value)
|
|
185
|
+
return false unless actual > decimal(schema["exclusiveMinimum"])
|
|
186
|
+
end
|
|
187
|
+
if schema.key?("multipleOf")
|
|
188
|
+
divisor = decimal(schema["multipleOf"])
|
|
189
|
+
return false unless divisor.positive?
|
|
190
|
+
return false unless (actual || decimal(value)).remainder(divisor).zero?
|
|
191
|
+
end
|
|
192
|
+
true
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private def valid_string?(node, value)
|
|
196
|
+
schema = node.schema
|
|
197
|
+
length = value.length
|
|
198
|
+
return false if schema.key?("maxLength") && length > schema["maxLength"]
|
|
199
|
+
return false if schema.key?("minLength") && length < schema["minLength"]
|
|
200
|
+
return false if schema.key?("pattern") && !ecma_regexp(schema["pattern"]).match?(value)
|
|
201
|
+
if format_asserted?(node)
|
|
202
|
+
return false unless node.format.call(value)
|
|
203
|
+
end
|
|
204
|
+
return valid_content?(schema, value) if @validate_content
|
|
205
|
+
|
|
206
|
+
true
|
|
207
|
+
rescue RegexpError, IPAddr::InvalidAddressError
|
|
208
|
+
false
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private def valid_content?(schema, value)
|
|
212
|
+
decoded = (schema["contentEncoding"] == "base64") ? Base64.strict_decode64(value) : value
|
|
213
|
+
JSON.parse(decoded) if schema["contentMediaType"] == "application/json"
|
|
214
|
+
true
|
|
215
|
+
rescue ArgumentError, JSON::ParserError
|
|
216
|
+
false
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private def valid_array?(node, value)
|
|
220
|
+
schema = node.schema
|
|
221
|
+
length = value.length
|
|
222
|
+
return false if schema.key?("maxItems") && length > schema["maxItems"]
|
|
223
|
+
return false if schema.key?("minItems") && length < schema["minItems"]
|
|
224
|
+
if schema["uniqueItems"]
|
|
225
|
+
value.each_with_index do |item, index|
|
|
226
|
+
return false if value[0...index].any? { |previous| json_equal?(previous, item) }
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
prefix_items = schema["prefixItems"]
|
|
231
|
+
if prefix_items.is_a?(Array)
|
|
232
|
+
prefix_items.each_index do |index|
|
|
233
|
+
break if index >= length
|
|
234
|
+
return false unless evaluate_valid(node.child("prefixItems", index), value[index])
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
items = schema["items"]
|
|
239
|
+
if items.is_a?(Array)
|
|
240
|
+
items.each_index do |index|
|
|
241
|
+
break if index >= length
|
|
242
|
+
return false unless evaluate_valid(node.child("items", index), value[index])
|
|
243
|
+
end
|
|
244
|
+
if length > items.length && schema.key?("additionalItems")
|
|
245
|
+
additional = node.child("additionalItems")
|
|
246
|
+
(items.length...length).each do |index|
|
|
247
|
+
return false unless evaluate_valid(additional, value[index])
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
elsif !items.nil?
|
|
251
|
+
child = node.child("items")
|
|
252
|
+
start = prefix_items.is_a?(Array) ? prefix_items.length : 0
|
|
253
|
+
(start...length).each do |index|
|
|
254
|
+
return false unless evaluate_valid(child, value[index])
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
if schema.key?("contains")
|
|
259
|
+
child = node.child("contains")
|
|
260
|
+
if node.dialect.keywords.key?("minContains")
|
|
261
|
+
matches = value.count { |item| evaluate_valid(child, item) }
|
|
262
|
+
return false if matches < schema.fetch("minContains", 1)
|
|
263
|
+
return false if schema.key?("maxContains") && matches > schema["maxContains"]
|
|
264
|
+
else
|
|
265
|
+
return false unless value.any? { |item| evaluate_valid(child, item) }
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
true
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
private def valid_object?(node, value)
|
|
272
|
+
schema = node.schema
|
|
273
|
+
length = value.length
|
|
274
|
+
return false if schema.key?("maxProperties") && length > schema["maxProperties"]
|
|
275
|
+
return false if schema.key?("minProperties") && length < schema["minProperties"]
|
|
276
|
+
return false if schema.key?("required") && !schema["required"].all? { |name| value.key?(name) }
|
|
277
|
+
|
|
278
|
+
properties = schema["properties"]
|
|
279
|
+
patterns = schema["patternProperties"]
|
|
280
|
+
additional = node.child("additionalProperties") if schema.key?("additionalProperties")
|
|
281
|
+
value.each do |name, property_value|
|
|
282
|
+
matched = false
|
|
283
|
+
if properties&.key?(name)
|
|
284
|
+
matched = true
|
|
285
|
+
return false unless evaluate_valid(node.child("properties", name), property_value)
|
|
286
|
+
end
|
|
287
|
+
if patterns
|
|
288
|
+
patterns.each_key do |pattern|
|
|
289
|
+
next unless ecma_regexp(pattern).match?(name)
|
|
290
|
+
matched = true
|
|
291
|
+
return false unless evaluate_valid(node.child("patternProperties", pattern), property_value)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
return false if !matched && additional && !evaluate_valid(additional, property_value)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
if schema.key?("propertyNames")
|
|
298
|
+
child = node.child("propertyNames")
|
|
299
|
+
value.each_key { |name| return false unless evaluate_valid(child, name) }
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
if schema.key?("dependencies")
|
|
303
|
+
schema["dependencies"].each do |name, dependency|
|
|
304
|
+
next unless value.key?(name)
|
|
305
|
+
if dependency.is_a?(Array)
|
|
306
|
+
return false unless dependency.all? { |required_name| value.key?(required_name) }
|
|
307
|
+
else
|
|
308
|
+
return false unless evaluate_valid(node.child("dependencies", name), value)
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
if schema.key?("dependentRequired")
|
|
313
|
+
schema["dependentRequired"].each do |name, required_names|
|
|
314
|
+
next unless value.key?(name)
|
|
315
|
+
return false unless required_names.all? { |required_name| value.key?(required_name) }
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
if schema.key?("dependentSchemas")
|
|
320
|
+
schema["dependentSchemas"].each_key do |name|
|
|
321
|
+
next unless value.key?(name)
|
|
322
|
+
return false unless evaluate_valid(node.child("dependentSchemas", name), value)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
true
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
private def evaluate(node, instance)
|
|
329
|
+
schema = node.schema
|
|
330
|
+
return Evaluation.valid if schema == true
|
|
331
|
+
if schema == false
|
|
332
|
+
add_error("falseSchema", "boolean schema is false", append_keyword: false)
|
|
333
|
+
return Evaluation.invalid
|
|
334
|
+
end
|
|
335
|
+
return Evaluation.valid unless schema.is_a?(Hash)
|
|
336
|
+
|
|
337
|
+
before = @error_count
|
|
338
|
+
evaluation = Evaluation.valid
|
|
339
|
+
|
|
340
|
+
if @track_dynamic_scope
|
|
341
|
+
entered_scope = @dynamic_scope.nil? || !@dynamic_scope.last.equal?(node.resource)
|
|
342
|
+
(@dynamic_scope ||= []) << node.resource if entered_scope
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
if schema.key?("$ref")
|
|
346
|
+
begin
|
|
347
|
+
target = @graph.resolve(node, schema["$ref"])
|
|
348
|
+
evaluation = evaluation.merge(
|
|
349
|
+
evaluate_reference(node, target, instance, "$ref")
|
|
350
|
+
)
|
|
351
|
+
rescue ResolutionError => e
|
|
352
|
+
add_error("$ref", e.message, append_keyword: false)
|
|
353
|
+
end
|
|
354
|
+
return (@error_count == before) ? evaluation : Evaluation.invalid unless node.dialect.ref_siblings?
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
if schema.key?("$recursiveRef")
|
|
358
|
+
target = recursive_target(node, schema["$recursiveRef"])
|
|
359
|
+
evaluation = evaluation.merge(evaluate_reference(node, target, instance, "$recursiveRef"))
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
if schema.key?("$dynamicRef")
|
|
363
|
+
target = dynamic_target(node, schema["$dynamicRef"])
|
|
364
|
+
evaluation = evaluation.merge(evaluate_reference(node, target, instance, "$dynamicRef"))
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
keywords = node.keyword_mask
|
|
368
|
+
categories = Internal::Dialect
|
|
369
|
+
check_type(schema, instance) if (keywords & categories::TYPE) != 0
|
|
370
|
+
check_enum(schema, instance) if (keywords & categories::ENUM) != 0
|
|
371
|
+
if (keywords & categories::COMBINER) != 0
|
|
372
|
+
evaluation = evaluation.merge(check_combiners(node, instance))
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
case instance
|
|
376
|
+
when Hash
|
|
377
|
+
if (keywords & categories::OBJECT) != 0
|
|
378
|
+
evaluation = evaluation.merge(check_object(node, instance, evaluation))
|
|
379
|
+
end
|
|
380
|
+
when Array
|
|
381
|
+
if (keywords & categories::ARRAY) != 0
|
|
382
|
+
evaluation = evaluation.merge(check_array(node, instance, evaluation))
|
|
383
|
+
end
|
|
384
|
+
when String
|
|
385
|
+
check_string(node, instance) if (keywords & categories::STRING) != 0 || format_asserted?(node)
|
|
386
|
+
when Numeric
|
|
387
|
+
check_number(schema, instance) if !instance.is_a?(Complex) && (keywords & categories::NUMBER) != 0
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
(@error_count == before) ? evaluation : Evaluation.invalid
|
|
391
|
+
ensure
|
|
392
|
+
@dynamic_scope.pop if entered_scope
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
private def evaluate_reference(node, target, instance, keyword)
|
|
396
|
+
instances = active_instances(node)
|
|
397
|
+
instance_id = instance.object_id
|
|
398
|
+
return Evaluation.valid if instances[instance_id]
|
|
399
|
+
|
|
400
|
+
instances[instance_id] = true
|
|
401
|
+
activated = true
|
|
402
|
+
evaluate_at(target, instance, MISSING_SEGMENT, keyword)
|
|
403
|
+
rescue ResolutionError => e
|
|
404
|
+
add_error(keyword, e.message, append_keyword: false)
|
|
405
|
+
Evaluation.invalid
|
|
406
|
+
ensure
|
|
407
|
+
instances&.delete(instance_id) if activated
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
private def recursive_target(node, reference)
|
|
411
|
+
target = @graph.resolve(node, reference)
|
|
412
|
+
return target unless reference.to_s.end_with?("#") && target.schema.is_a?(Hash) && target.schema["$recursiveAnchor"] == true
|
|
413
|
+
|
|
414
|
+
@dynamic_scope.filter_map { |resource| resource.root if resource.root.schema.is_a?(Hash) && resource.root.schema["$recursiveAnchor"] == true }.first || target
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
private def dynamic_target(node, reference)
|
|
418
|
+
target = @graph.resolve(node, reference)
|
|
419
|
+
raw_fragment = reference.to_s.split("#", 2)[1]
|
|
420
|
+
return target if raw_fragment.nil? || raw_fragment.empty? || raw_fragment.start_with?("/")
|
|
421
|
+
return target unless target.schema.is_a?(Hash) && target.schema["$dynamicAnchor"] == raw_fragment
|
|
422
|
+
|
|
423
|
+
@dynamic_scope.each do |resource|
|
|
424
|
+
dynamic = @graph.dynamic_anchor(resource, raw_fragment)
|
|
425
|
+
return dynamic if dynamic
|
|
426
|
+
end
|
|
427
|
+
target
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
private def active_instances(node)
|
|
431
|
+
active = (@active ||= {})
|
|
432
|
+
active[node.object_id] ||= {}
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
private def check_type(schema, value)
|
|
436
|
+
return unless schema.key?("type")
|
|
437
|
+
|
|
438
|
+
types = Array(schema["type"])
|
|
439
|
+
return if types.any? { |type| type?(value, type) }
|
|
440
|
+
|
|
441
|
+
add_error("type", "expected #{types.join(" or ")}")
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
private def check_enum(schema, value)
|
|
445
|
+
if schema.key?("enum") && !schema["enum"].any? { |candidate| json_equal?(candidate, value) }
|
|
446
|
+
add_error("enum", "value is not in enum")
|
|
447
|
+
end
|
|
448
|
+
if schema.key?("const") && !json_equal?(schema["const"], value)
|
|
449
|
+
add_error("const", "value does not equal const")
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
private def check_combiners(node, value)
|
|
454
|
+
schema = node.schema
|
|
455
|
+
evaluation = Evaluation.valid
|
|
456
|
+
if schema.key?("allOf")
|
|
457
|
+
schema["allOf"].each_index do |index|
|
|
458
|
+
evaluation = evaluation.merge(
|
|
459
|
+
evaluate_at(node.child("allOf", index), value, MISSING_SEGMENT, "allOf", index)
|
|
460
|
+
)
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
if schema.key?("anyOf")
|
|
465
|
+
matches = []
|
|
466
|
+
schema["anyOf"].each_index do |index|
|
|
467
|
+
result = trial_at(node.child("anyOf", index), value, MISSING_SEGMENT, "anyOf", index)
|
|
468
|
+
matches << result if result.valid?
|
|
469
|
+
end
|
|
470
|
+
if matches.empty?
|
|
471
|
+
add_error("anyOf", "no subschema matched")
|
|
472
|
+
else
|
|
473
|
+
matches.each { |result| evaluation = evaluation.merge(result) }
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
if schema.key?("oneOf")
|
|
478
|
+
matches = []
|
|
479
|
+
schema["oneOf"].each_index do |index|
|
|
480
|
+
result = trial_at(node.child("oneOf", index), value, MISSING_SEGMENT, "oneOf", index)
|
|
481
|
+
matches << result if result.valid?
|
|
482
|
+
end
|
|
483
|
+
if matches.length == 1
|
|
484
|
+
evaluation = evaluation.merge(matches.first)
|
|
485
|
+
else
|
|
486
|
+
add_error("oneOf", "expected exactly one match, got #{matches.length}")
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
if schema.key?("not") && trial_at(node.child("not"), value, MISSING_SEGMENT, "not").valid?
|
|
491
|
+
add_error("not", "subschema matched")
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
if schema.key?("if")
|
|
495
|
+
condition = trial_at(node.child("if"), value, MISSING_SEGMENT, "if")
|
|
496
|
+
branch = condition.valid? ? "then" : "else"
|
|
497
|
+
evaluation = evaluation.merge(condition) if condition.valid?
|
|
498
|
+
if schema.key?(branch)
|
|
499
|
+
evaluation = evaluation.merge(evaluate_at(node.child(branch), value, MISSING_SEGMENT, branch))
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
evaluation
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
private def check_number(schema, value)
|
|
506
|
+
compare(schema, "maximum", value) { |a, b| a <= b }
|
|
507
|
+
compare(schema, "minimum", value) { |a, b| a >= b }
|
|
508
|
+
compare(schema, "exclusiveMaximum", value) { |a, b| a < b }
|
|
509
|
+
compare(schema, "exclusiveMinimum", value) { |a, b| a > b }
|
|
510
|
+
|
|
511
|
+
return unless schema.key?("multipleOf")
|
|
512
|
+
|
|
513
|
+
divisor = decimal(schema["multipleOf"])
|
|
514
|
+
valid = divisor.positive? && decimal(value).remainder(divisor).zero?
|
|
515
|
+
add_error("multipleOf", "number is not a multiple") unless valid
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
private def compare(schema, keyword, value)
|
|
519
|
+
return unless schema.key?(keyword)
|
|
520
|
+
return if yield(decimal(value), decimal(schema[keyword]))
|
|
521
|
+
|
|
522
|
+
add_error(keyword, "numeric limit was exceeded")
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
private def check_string(node, value)
|
|
526
|
+
schema = node.schema
|
|
527
|
+
length = value.length
|
|
528
|
+
limit(schema, "maxLength", length) { |actual, expected| actual <= expected }
|
|
529
|
+
limit(schema, "minLength", length) { |actual, expected| actual >= expected }
|
|
530
|
+
|
|
531
|
+
if schema.key?("pattern")
|
|
532
|
+
matched = ecma_regexp(schema["pattern"]).match?(value)
|
|
533
|
+
add_error("pattern", "string does not match pattern") unless matched
|
|
534
|
+
end
|
|
535
|
+
if format_asserted?(node)
|
|
536
|
+
add_error("format", "string is not a valid #{node.format.name}") unless node.format.call(value)
|
|
537
|
+
end
|
|
538
|
+
check_content(schema, value) if @validate_content
|
|
539
|
+
rescue RegexpError
|
|
540
|
+
add_error("pattern", "invalid regular expression")
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
private def format_asserted?(node)
|
|
544
|
+
(@validate_format || node.dialect.format_assertion?) && !node.format.nil?
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
private def check_array(node, value, prior_evaluation)
|
|
548
|
+
schema = node.schema
|
|
549
|
+
evaluated = []
|
|
550
|
+
limit(schema, "maxItems", value.length) { |actual, expected| actual <= expected }
|
|
551
|
+
limit(schema, "minItems", value.length) { |actual, expected| actual >= expected }
|
|
552
|
+
|
|
553
|
+
if schema["uniqueItems"]
|
|
554
|
+
duplicate = value.each_with_index.any? do |item, index|
|
|
555
|
+
value[0...index].any? { |previous| json_equal?(previous, item) }
|
|
556
|
+
end
|
|
557
|
+
add_error("uniqueItems", "array items are not unique") if duplicate
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
prefix_items = schema["prefixItems"]
|
|
561
|
+
if prefix_items.is_a?(Array)
|
|
562
|
+
prefix_items.each_index do |index|
|
|
563
|
+
break if index >= value.length
|
|
564
|
+
evaluate_at(node.child("prefixItems", index), value[index], index, "prefixItems", index)
|
|
565
|
+
evaluated << index
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
items = schema["items"]
|
|
570
|
+
if items.is_a?(Array)
|
|
571
|
+
items.each_index do |index|
|
|
572
|
+
break if index >= value.length
|
|
573
|
+
evaluate_at(node.child("items", index), value[index], index, "items", index)
|
|
574
|
+
evaluated << index
|
|
575
|
+
end
|
|
576
|
+
if value.length > items.length && schema.key?("additionalItems")
|
|
577
|
+
additional = node.child("additionalItems")
|
|
578
|
+
(items.length...value.length).each do |index|
|
|
579
|
+
evaluate_at(additional, value[index], index, "additionalItems")
|
|
580
|
+
evaluated << index
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
elsif !items.nil?
|
|
584
|
+
start = prefix_items.is_a?(Array) ? prefix_items.length : 0
|
|
585
|
+
value.each_with_index do |item, index|
|
|
586
|
+
next if index < start
|
|
587
|
+
evaluate_at(node.child("items"), item, index, "items")
|
|
588
|
+
evaluated << index
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
if schema.key?("contains")
|
|
593
|
+
matched = value.each_index.select do |index|
|
|
594
|
+
trial_at(node.child("contains"), value[index], index, "contains").valid?
|
|
595
|
+
end
|
|
596
|
+
minimum = schema.fetch("minContains", 1)
|
|
597
|
+
maximum = schema.fetch("maxContains", Float::INFINITY)
|
|
598
|
+
unless matched.length.between?(minimum, maximum)
|
|
599
|
+
add_error("contains", "matched #{matched.length} array items")
|
|
600
|
+
end
|
|
601
|
+
evaluated.concat(matched)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
combined = prior_evaluation.evaluated_items | evaluated
|
|
605
|
+
if schema.key?("unevaluatedItems")
|
|
606
|
+
unevaluated = (0...value.length).to_a - combined
|
|
607
|
+
unevaluated.each do |index|
|
|
608
|
+
evaluate_at(node.child("unevaluatedItems"), value[index], index, "unevaluatedItems")
|
|
609
|
+
end
|
|
610
|
+
evaluated.concat(unevaluated)
|
|
611
|
+
end
|
|
612
|
+
Evaluation.valid(evaluated_items: evaluated.uniq)
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
private def check_object(node, value, prior_evaluation)
|
|
616
|
+
schema = node.schema
|
|
617
|
+
evaluated = []
|
|
618
|
+
limit(schema, "maxProperties", value.length) { |actual, expected| actual <= expected }
|
|
619
|
+
limit(schema, "minProperties", value.length) { |actual, expected| actual >= expected }
|
|
620
|
+
|
|
621
|
+
Array(schema["required"]).each do |name|
|
|
622
|
+
add_error("required", "required property #{name.inspect} is missing") unless value.key?(name)
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
properties = schema.fetch("properties", {})
|
|
626
|
+
patterns = schema.fetch("patternProperties", {})
|
|
627
|
+
value.each do |name, property_value|
|
|
628
|
+
matched = false
|
|
629
|
+
if properties.key?(name)
|
|
630
|
+
matched = true
|
|
631
|
+
evaluate_at(node.child("properties", name), property_value, name, "properties", name)
|
|
632
|
+
evaluated << name
|
|
633
|
+
end
|
|
634
|
+
patterns.each do |pattern, subschema|
|
|
635
|
+
next unless ecma_regexp(pattern).match?(name)
|
|
636
|
+
matched = true
|
|
637
|
+
evaluate_at(node.child("patternProperties", pattern), property_value, name, "patternProperties", pattern)
|
|
638
|
+
evaluated << name
|
|
639
|
+
end
|
|
640
|
+
if !matched && schema.key?("additionalProperties")
|
|
641
|
+
evaluate_at(node.child("additionalProperties"), property_value, name, "additionalProperties")
|
|
642
|
+
evaluated << name
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
if schema.key?("propertyNames")
|
|
647
|
+
value.each_key do |name|
|
|
648
|
+
evaluate_at(node.child("propertyNames"), name, name, "propertyNames")
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
schema.fetch("dependencies", {}).each do |name, dependency|
|
|
653
|
+
next unless value.key?(name)
|
|
654
|
+
if dependency.is_a?(Array)
|
|
655
|
+
dependency.each do |required_name|
|
|
656
|
+
add_error("dependencies", "property #{required_name.inspect} is required by #{name.inspect}") unless value.key?(required_name)
|
|
657
|
+
end
|
|
658
|
+
else
|
|
659
|
+
result = evaluate_at(node.child("dependencies", name), value, MISSING_SEGMENT, "dependencies", name)
|
|
660
|
+
evaluated.concat(result.evaluated_properties) if result.valid?
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
schema.fetch("dependentRequired", {}).each do |name, required_names|
|
|
665
|
+
next unless value.key?(name)
|
|
666
|
+
required_names.each do |required_name|
|
|
667
|
+
unless value.key?(required_name)
|
|
668
|
+
add_error("dependentRequired", "property #{required_name.inspect} is required by #{name.inspect}")
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
schema.fetch("dependentSchemas", {}).each_key do |name|
|
|
674
|
+
next unless value.key?(name)
|
|
675
|
+
result = evaluate_at(node.child("dependentSchemas", name), value, MISSING_SEGMENT, "dependentSchemas", name)
|
|
676
|
+
evaluated.concat(result.evaluated_properties) if result.valid?
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
combined = prior_evaluation.evaluated_properties | evaluated
|
|
680
|
+
if schema.key?("unevaluatedProperties")
|
|
681
|
+
unevaluated = value.keys - combined
|
|
682
|
+
unevaluated.each do |name|
|
|
683
|
+
evaluate_at(node.child("unevaluatedProperties"), value[name], name, "unevaluatedProperties")
|
|
684
|
+
end
|
|
685
|
+
evaluated.concat(unevaluated)
|
|
686
|
+
end
|
|
687
|
+
Evaluation.valid(evaluated_properties: evaluated.uniq)
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
private def trial(node, value)
|
|
691
|
+
saved_callback = @error_callback
|
|
692
|
+
saved_error_count = @error_count
|
|
693
|
+
@error_callback = nil
|
|
694
|
+
@error_count = 0
|
|
695
|
+
result = evaluate(node, value)
|
|
696
|
+
result
|
|
697
|
+
ensure
|
|
698
|
+
@error_callback = saved_callback
|
|
699
|
+
@error_count = saved_error_count
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
private def limit(schema, keyword, actual)
|
|
703
|
+
return unless schema.key?(keyword)
|
|
704
|
+
return if yield(actual, schema[keyword])
|
|
705
|
+
|
|
706
|
+
add_error(keyword, "size limit was exceeded")
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
private def type?(value, type)
|
|
710
|
+
case type
|
|
711
|
+
when "null" then value.nil?
|
|
712
|
+
when "boolean" then value == true || value == false
|
|
713
|
+
when "object" then value.is_a?(Hash)
|
|
714
|
+
when "array" then value.is_a?(Array)
|
|
715
|
+
when "number" then number?(value)
|
|
716
|
+
when "integer" then number?(value) && value.finite? && value.to_i == value
|
|
717
|
+
when "string" then value.is_a?(String)
|
|
718
|
+
else false
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
private def number?(value)
|
|
723
|
+
value.is_a?(Numeric) && !value.is_a?(Complex)
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
private def json_equal?(left, right)
|
|
727
|
+
return false if json_kind(left) != json_kind(right)
|
|
728
|
+
case left
|
|
729
|
+
when Hash
|
|
730
|
+
left.length == right.length && left.all? { |key, value| right.key?(key) && json_equal?(value, right[key]) }
|
|
731
|
+
when Array
|
|
732
|
+
left.length == right.length && left.each_index.all? { |index| json_equal?(left[index], right[index]) }
|
|
733
|
+
else
|
|
734
|
+
left == right
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
private def json_kind(value)
|
|
739
|
+
return :number if number?(value)
|
|
740
|
+
return :boolean if value == true || value == false
|
|
741
|
+
value.class
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
private def decimal(value)
|
|
745
|
+
return value if value.is_a?(Integer) || value.is_a?(Rational)
|
|
746
|
+
|
|
747
|
+
Rational(value.to_s)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# Ruby and ECMA-262 differ in their ASCII character classes, anchors, and
|
|
751
|
+
# definition of whitespace. Draft 7 patterns use the ECMA behavior.
|
|
752
|
+
private def ecma_regexp(pattern)
|
|
753
|
+
regexps = (@regexps ||= {})
|
|
754
|
+
return regexps[pattern] if regexps.key?(pattern)
|
|
755
|
+
|
|
756
|
+
whitespace = "\\u0009-\\u000D\\u0020\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000\\uFEFF"
|
|
757
|
+
translated = +""
|
|
758
|
+
escaped = false
|
|
759
|
+
in_class = false
|
|
760
|
+
pattern.each_char do |character|
|
|
761
|
+
if escaped
|
|
762
|
+
translated << case character
|
|
763
|
+
when "d" then in_class ? "0-9" : "[0-9]"
|
|
764
|
+
when "D" then in_class ? "^0-9" : "[^0-9]"
|
|
765
|
+
when "w" then in_class ? "A-Za-z0-9_" : "[A-Za-z0-9_]"
|
|
766
|
+
when "W" then in_class ? "^A-Za-z0-9_" : "[^A-Za-z0-9_]"
|
|
767
|
+
when "s" then in_class ? whitespace : "[#{whitespace}]"
|
|
768
|
+
when "S" then in_class ? "^#{whitespace}" : "[^#{whitespace}]"
|
|
769
|
+
else "\\#{character}"
|
|
770
|
+
end
|
|
771
|
+
escaped = false
|
|
772
|
+
elsif character == "\\"
|
|
773
|
+
escaped = true
|
|
774
|
+
elsif character == "["
|
|
775
|
+
in_class = true
|
|
776
|
+
translated << character
|
|
777
|
+
elsif character == "]"
|
|
778
|
+
in_class = false
|
|
779
|
+
translated << character
|
|
780
|
+
elsif character == "^" && !in_class
|
|
781
|
+
translated << "\\A"
|
|
782
|
+
elsif character == "$" && !in_class
|
|
783
|
+
translated << "\\z"
|
|
784
|
+
else
|
|
785
|
+
translated << character
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
translated << "\\" if escaped
|
|
789
|
+
regexps[pattern] = Regexp.new(translated)
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
private def check_content(schema, value)
|
|
793
|
+
decoded = value
|
|
794
|
+
if schema["contentEncoding"] == "base64"
|
|
795
|
+
decoded = Base64.strict_decode64(value)
|
|
796
|
+
end
|
|
797
|
+
return unless schema["contentMediaType"] == "application/json"
|
|
798
|
+
|
|
799
|
+
JSON.parse(decoded)
|
|
800
|
+
rescue ArgumentError, JSON::ParserError
|
|
801
|
+
keyword = (schema["contentEncoding"] == "base64") ? "contentEncoding" : "contentMediaType"
|
|
802
|
+
add_error(keyword, "string content is invalid")
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
private def add_error(keyword, message, append_keyword: true)
|
|
806
|
+
@error_count += 1
|
|
807
|
+
if @error_callback
|
|
808
|
+
schema_keyword = append_keyword ? keyword : MISSING_SEGMENT
|
|
809
|
+
@error_callback.call(
|
|
810
|
+
ValidationError.new(
|
|
811
|
+
keyword: keyword,
|
|
812
|
+
instance_path: pointer(@instance_path),
|
|
813
|
+
schema_path: pointer(@schema_path, schema_keyword),
|
|
814
|
+
message: message
|
|
815
|
+
)
|
|
816
|
+
)
|
|
817
|
+
end
|
|
818
|
+
false
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
private def evaluate_at(node, instance, instance_segment, schema_segment, schema_child_segment = MISSING_SEGMENT)
|
|
822
|
+
@instance_path << instance_segment unless instance_segment.equal?(MISSING_SEGMENT)
|
|
823
|
+
@schema_path << schema_segment
|
|
824
|
+
@schema_path << schema_child_segment unless schema_child_segment.equal?(MISSING_SEGMENT)
|
|
825
|
+
evaluate(node, instance)
|
|
826
|
+
ensure
|
|
827
|
+
@schema_path.pop unless schema_child_segment.equal?(MISSING_SEGMENT)
|
|
828
|
+
@schema_path.pop
|
|
829
|
+
@instance_path.pop unless instance_segment.equal?(MISSING_SEGMENT)
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
private def trial_at(node, instance, instance_segment, schema_segment, schema_child_segment = MISSING_SEGMENT)
|
|
833
|
+
@instance_path << instance_segment unless instance_segment.equal?(MISSING_SEGMENT)
|
|
834
|
+
@schema_path << schema_segment
|
|
835
|
+
@schema_path << schema_child_segment unless schema_child_segment.equal?(MISSING_SEGMENT)
|
|
836
|
+
trial(node, instance)
|
|
837
|
+
ensure
|
|
838
|
+
@schema_path.pop unless schema_child_segment.equal?(MISSING_SEGMENT)
|
|
839
|
+
@schema_path.pop
|
|
840
|
+
@instance_path.pop unless instance_segment.equal?(MISSING_SEGMENT)
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
private def pointer(path, final_segment = MISSING_SEGMENT)
|
|
844
|
+
pointer = +""
|
|
845
|
+
path.each { |segment| append_pointer_segment(pointer, segment) }
|
|
846
|
+
append_pointer_segment(pointer, final_segment) unless final_segment.equal?(MISSING_SEGMENT)
|
|
847
|
+
pointer
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
private def append_pointer_segment(pointer, segment)
|
|
851
|
+
pointer << "/" << segment.to_s.gsub("~", "~0").gsub("/", "~1")
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
private_constant :MISSING_SEGMENT
|
|
855
|
+
end
|
|
856
|
+
end
|
|
857
|
+
end
|