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