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.
@@ -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