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