schemurai 2.1.0 → 2.2.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.
@@ -8,8 +8,49 @@ require_relative "compiler"
8
8
 
9
9
  module Schemurai
10
10
  module VM
11
+ class EvaluationBuffer
12
+ attr_reader :evaluated_properties, :evaluated_items
13
+
14
+ def initialize
15
+ @evaluated_properties = []
16
+ @evaluated_items = []
17
+ end
18
+
19
+ def valid? = true
20
+
21
+ def reset
22
+ @evaluated_properties.clear
23
+ @evaluated_items.clear
24
+ self
25
+ end
26
+
27
+ def record_property(name)
28
+ @evaluated_properties << name unless @evaluated_properties.include?(name)
29
+ self
30
+ end
31
+
32
+ def record_item(index)
33
+ @evaluated_items << index unless @evaluated_items.include?(index)
34
+ self
35
+ end
36
+
37
+ def merge(other)
38
+ return Evaluation.invalid unless other.valid?
39
+
40
+ merge_locations(@evaluated_properties, other.evaluated_properties)
41
+ merge_locations(@evaluated_items, other.evaluated_items)
42
+ self
43
+ end
44
+
45
+ private def merge_locations(target, locations)
46
+ locations.each { |location| target << location unless target.include?(location) }
47
+ end
48
+ end
49
+ private_constant :EvaluationBuffer
50
+
11
51
  class Evaluator
12
52
  MISSING_SEGMENT = Object.new.freeze
53
+ DECIMAL_CACHE_LIMIT = 16
13
54
 
14
55
  def backend = :vm
15
56
 
@@ -21,6 +62,14 @@ module Schemurai
21
62
  @validate_format = format
22
63
  @regexps = nil
23
64
  @active = nil
65
+ @resolved_references = nil
66
+ @decimals = nil
67
+ @multiple_results = nil
68
+ @dynamic_scope = nil
69
+ @instance_path_buffer = nil
70
+ @schema_path_buffer = nil
71
+ @evaluation_pool = nil
72
+ @evaluation_pool_index = 0
24
73
  end
25
74
 
26
75
  def validate(instance)
@@ -34,31 +83,52 @@ module Schemurai
34
83
 
35
84
  def valid?(instance)
36
85
  @errors = nil
37
- prepare_evaluation(paths: @root.tracks_evaluation?)
86
+ @error_count = 0
87
+ @dynamic_scope&.clear
88
+ @instance_path = nil
89
+ @schema_path = nil
90
+ reset_evaluation_pool
38
91
  evaluate_valid(@root, instance)
39
92
  end
40
93
 
41
94
  private def prepare_evaluation(paths:)
42
95
  @error_count = 0
43
- @dynamic_scope = nil
44
- @instance_path = paths ? [] : nil
45
- @schema_path = paths ? [] : nil
96
+ @dynamic_scope&.clear
97
+ reset_evaluation_pool
98
+ if paths
99
+ (@instance_path_buffer ||= []).clear
100
+ (@schema_path_buffer ||= []).clear
101
+ @instance_path = @instance_path_buffer
102
+ @schema_path = @schema_path_buffer
103
+ else
104
+ @instance_path = nil
105
+ @schema_path = nil
106
+ end
46
107
  end
47
108
 
48
109
  private def evaluate_valid(program, instance)
49
- if program.tracks_evaluation?
50
- @instance_path ||= []
51
- @schema_path ||= []
52
- return evaluate(program, instance).valid?
110
+ code = program.code
111
+ flags = code[0]
112
+ entered_scope = false
113
+ unless flags.zero?
114
+ return evaluate(program, instance).valid? if (flags & TRACKS_EVALUATION) != 0
115
+
116
+ if (flags & TRACKS_DYNAMIC_SCOPE) != 0
117
+ resource = program.node.resource
118
+ unless @dynamic_scope&.last.equal?(resource)
119
+ (@dynamic_scope ||= []) << resource
120
+ entered_scope = true
121
+ end
122
+ end
53
123
  end
54
-
55
- entered_scope = enter_scope(program)
56
- program.code.each do |opcode, operand, extra|
124
+ instruction_index = 1
125
+ while (opcode = code[instruction_index])
126
+ operand = code[instruction_index + 1]
57
127
  case opcode
58
128
  when :boolean
59
129
  return operand
60
130
  when :ref
61
- target = @compiler.resolve(program, operand.value)
131
+ target = reference_target(program, operand)
62
132
  return false unless valid_reference?(program, target, instance)
63
133
  when :recursive_ref
64
134
  target = recursive_target(program, operand)
@@ -75,11 +145,22 @@ module Schemurai
75
145
  when :type_array
76
146
  return false unless instance.is_a?(Array)
77
147
  when :type_number
78
- return false unless number?(instance)
148
+ return false unless instance.is_a?(Numeric) && !instance.is_a?(Complex)
79
149
  when :type_integer
80
- return false unless integer?(instance)
150
+ return false unless instance.is_a?(Numeric) && !instance.is_a?(Complex) && instance.finite? && instance.to_i == instance
81
151
  when :type_string
82
152
  return false unless instance.is_a?(String)
153
+ when :typed_number
154
+ return false unless instance.is_a?(Numeric) && !instance.is_a?(Complex) && valid_number?(operand, instance)
155
+ when :typed_integer
156
+ return false unless instance.is_a?(Numeric) && !instance.is_a?(Complex) && instance.finite? &&
157
+ instance.to_i == instance && valid_number?(operand, instance)
158
+ when :typed_string
159
+ return false unless instance.is_a?(String) && valid_string?(operand, instance)
160
+ when :typed_array
161
+ return false unless instance.is_a?(Array) && valid_array?(operand, instance)
162
+ when :typed_object
163
+ return false unless instance.is_a?(Hash) && valid_object?(operand, instance)
83
164
  when :types
84
165
  return false if (operand.mask & instance_type_mask(instance, integer: (operand.mask & TYPE_INTEGER) != 0)).zero?
85
166
  when :enum
@@ -101,8 +182,8 @@ module Schemurai
101
182
  return false if evaluate_valid(operand, instance)
102
183
  when :conditional
103
184
  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)
185
+ return false if operand.then_branch && !evaluate_valid(operand.then_branch, instance)
186
+ elsif operand.else_branch && !evaluate_valid(operand.else_branch, instance)
106
187
  return false
107
188
  end
108
189
  when :number
@@ -116,6 +197,7 @@ module Schemurai
116
197
  else
117
198
  raise "unknown VM instruction #{opcode.inspect}"
118
199
  end
200
+ instruction_index += 2
119
201
  end
120
202
  true
121
203
  rescue ResolutionError
@@ -124,12 +206,34 @@ module Schemurai
124
206
  leave_scope if entered_scope
125
207
  end
126
208
 
209
+ private def reset_evaluation_pool
210
+ @evaluation_pool_index = 0
211
+ end
212
+
213
+ private def tracked_evaluation
214
+ pool = (@evaluation_pool ||= [])
215
+ index = @evaluation_pool_index
216
+ @evaluation_pool_index += 1
217
+ (pool[index] ||= EvaluationBuffer.new).reset
218
+ end
219
+
127
220
  private def evaluate(program, instance)
128
221
  before = @error_count
129
222
  evaluation = Evaluation.valid
130
- entered_scope = enter_scope(program)
223
+ code = program.code
224
+ flags = code[0]
225
+ entered_scope = false
226
+ if (flags & TRACKS_DYNAMIC_SCOPE) != 0
227
+ resource = program.node.resource
228
+ unless @dynamic_scope&.last.equal?(resource)
229
+ (@dynamic_scope ||= []) << resource
230
+ entered_scope = true
231
+ end
232
+ end
131
233
 
132
- program.code.each do |opcode, operand, extra|
234
+ instruction_index = 1
235
+ while (opcode = code[instruction_index])
236
+ operand = code[instruction_index + 1]
133
237
  case opcode
134
238
  when :boolean
135
239
  if operand == false
@@ -156,6 +260,42 @@ module Schemurai
156
260
  check_compiled_type("integer", integer?(instance), instance)
157
261
  when :type_string
158
262
  check_compiled_type("string", instance.is_a?(String), instance)
263
+ when :typed_number
264
+ if number?(instance)
265
+ check_number(operand, instance)
266
+ else
267
+ check_compiled_type("number", false, instance)
268
+ end
269
+ when :typed_integer
270
+ if instance.is_a?(Numeric) && !instance.is_a?(Complex)
271
+ integer = instance.finite? && instance.to_i == instance
272
+ check_compiled_type("integer", integer, instance)
273
+ check_number(operand, instance)
274
+ else
275
+ check_compiled_type("integer", false, instance)
276
+ end
277
+ when :typed_string
278
+ if instance.is_a?(String)
279
+ check_string(operand, instance)
280
+ else
281
+ check_compiled_type("string", false, instance)
282
+ end
283
+ when :typed_array
284
+ if instance.is_a?(Array)
285
+ result = check_array(operand, instance, evaluation)
286
+ return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
287
+ evaluation = evaluation.merge(result)
288
+ else
289
+ check_compiled_type("array", false, instance)
290
+ end
291
+ when :typed_object
292
+ if instance.is_a?(Hash)
293
+ result = check_object(operand, instance, evaluation)
294
+ return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
295
+ evaluation = evaluation.merge(result)
296
+ else
297
+ check_compiled_type("object", false, instance)
298
+ end
159
299
  when :types
160
300
  check_compiled_types(operand, instance)
161
301
  when :enum
@@ -163,50 +303,52 @@ module Schemurai
163
303
  when :const
164
304
  add_error("const") { Internal::ErrorMessage.const } unless json_equal?(operand, instance)
165
305
  when :allOf, :anyOf, :oneOf, :not, :conditional
166
- evaluation = evaluation.merge(check_combiner(opcode, operand, instance))
306
+ result = check_combiner(opcode, operand, instance)
307
+ return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
308
+ evaluation = evaluation.merge(result)
167
309
  when :number
168
310
  check_number(operand, instance) if instance.is_a?(Numeric) && !instance.is_a?(Complex)
169
311
  when :string
170
312
  check_string(operand, instance) if instance.is_a?(String)
171
313
  when :array
172
- evaluation = evaluation.merge(check_array(operand, instance, evaluation)) if instance.is_a?(Array)
314
+ if instance.is_a?(Array)
315
+ result = check_array(operand, instance, evaluation)
316
+ return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
317
+ evaluation = evaluation.merge(result)
318
+ end
173
319
  when :object
174
- evaluation = evaluation.merge(check_object(operand, instance, evaluation)) if instance.is_a?(Hash)
320
+ if instance.is_a?(Hash)
321
+ result = check_object(operand, instance, evaluation)
322
+ return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
323
+ evaluation = evaluation.merge(result)
324
+ end
175
325
  else
176
326
  raise "unknown VM instruction #{opcode.inspect}"
177
327
  end
328
+ instruction_index += 2
178
329
  end
179
330
  (@error_count == before) ? evaluation : Evaluation.invalid
180
331
  ensure
181
332
  leave_scope if entered_scope
182
333
  end
183
334
 
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
335
  private def leave_scope
193
336
  @dynamic_scope.pop
194
337
  end
195
338
 
196
339
  private def recursive_target(program, rules)
197
- target = @compiler.resolve(program, rules.value)
340
+ target = reference_target(program, rules)
198
341
  return target unless rules.fragment == "" && target.recursive_anchor?
199
342
 
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
343
+ @dynamic_scope&.each do |resource|
344
+ compiled = @compiler.compile(resource.root)
345
+ return compiled if compiled.recursive_anchor?
346
+ end
347
+ target
206
348
  end
207
349
 
208
350
  private def dynamic_target(program, rules)
209
- target = @compiler.resolve(program, rules.value)
351
+ target = reference_target(program, rules)
210
352
  fragment = rules.fragment
211
353
  return target if fragment.nil? || fragment.empty? || fragment.start_with?("/")
212
354
 
@@ -221,18 +363,22 @@ module Schemurai
221
363
 
222
364
  private def valid_reference?(source, target, instance)
223
365
  instances = active_instances(source)
224
- instance_id = instance.object_id
225
- return true if instances[instance_id]
366
+ return true if instances.key?(instance)
226
367
 
227
- instances[instance_id] = true
368
+ instances[instance] = true
228
369
  activated = true
229
370
  evaluate_valid(target, instance)
230
371
  ensure
231
- instances&.delete(instance_id) if activated
372
+ instances&.delete(instance) if activated
373
+ end
374
+
375
+ private def reference_target(program, rules)
376
+ references = (@resolved_references ||= {}.compare_by_identity)
377
+ references.fetch(rules) { references[rules] = @compiler.resolve(program, rules.value) }
232
378
  end
233
379
 
234
380
  private def evaluate_ref(source, rules, instance)
235
- target = @compiler.resolve(source, rules.value)
381
+ target = reference_target(source, rules)
236
382
  evaluate_reference(source, target, instance, "$ref")
237
383
  rescue ResolutionError => error
238
384
  unresolved_reference("$ref", error)
@@ -252,14 +398,13 @@ module Schemurai
252
398
 
253
399
  private def evaluate_reference(source, target, instance, keyword)
254
400
  instances = active_instances(source)
255
- instance_id = instance.object_id
256
- return Evaluation.valid if instances[instance_id]
401
+ return Evaluation.valid if instances.key?(instance)
257
402
 
258
- instances[instance_id] = true
403
+ instances[instance] = true
259
404
  activated = true
260
405
  evaluate_at(target, instance, MISSING_SEGMENT, keyword)
261
406
  ensure
262
- instances&.delete(instance_id) if activated
407
+ instances&.delete(instance) if activated
263
408
  end
264
409
 
265
410
  private def unresolved_reference(keyword, error)
@@ -268,8 +413,8 @@ module Schemurai
268
413
  end
269
414
 
270
415
  private def active_instances(program)
271
- active = (@active ||= {})
272
- active[program.node.object_id] ||= {}
416
+ active = (@active ||= {}.compare_by_identity)
417
+ active[program] ||= {}.compare_by_identity
273
418
  end
274
419
 
275
420
  private def check_compiled_type(name, valid, value)
@@ -301,19 +446,19 @@ module Schemurai
301
446
 
302
447
  private def valid_number?(rules, value)
303
448
  mask = rules.mask
304
- actual = decimal(value)
449
+ actual = (mask & MULTIPLE_OF).zero? ? value : decimal(value)
305
450
  return false if (mask & MAXIMUM) != 0 && actual > rules.maximum
306
451
  return false if (mask & MINIMUM) != 0 && actual < rules.minimum
307
452
  return false if (mask & EXCLUSIVE_MAXIMUM) != 0 && actual >= rules.exclusive_maximum
308
453
  return false if (mask & EXCLUSIVE_MINIMUM) != 0 && actual <= rules.exclusive_minimum
309
454
  return true if (mask & MULTIPLE_OF).zero?
310
455
 
311
- rules.multiple_of.positive? && actual.remainder(rules.multiple_of).zero?
456
+ valid_multiple?(rules, value)
312
457
  end
313
458
 
314
459
  private def check_number(rules, value)
315
460
  mask = rules.mask
316
- actual = decimal(value)
461
+ actual = (mask & MULTIPLE_OF).zero? ? value : decimal(value)
317
462
  if (mask & MAXIMUM) != 0 && actual > rules.maximum
318
463
  add_error("maximum") { Internal::ErrorMessage.numeric_limit("maximum", rules.maximum) }
319
464
  end
@@ -333,19 +478,31 @@ module Schemurai
333
478
  return if (mask & MULTIPLE_OF).zero?
334
479
 
335
480
  divisor = rules.multiple_of
336
- valid = divisor.positive? && actual.remainder(divisor).zero?
481
+ valid = valid_multiple?(rules, value)
337
482
  add_error("multipleOf") { Internal::ErrorMessage.multiple_of(divisor) } unless valid
338
483
  end
339
484
 
485
+ private def valid_multiple?(rules, value)
486
+ results = (@multiple_results ||= {}.compare_by_identity)
487
+ values = (results[rules] ||= {})
488
+ return values[value] if values.key?(value)
489
+
490
+ values.clear if values.length >= DECIMAL_CACHE_LIMIT
491
+ divisor = rules.multiple_of
492
+ values[value] = divisor.positive? && decimal(value).remainder(divisor).zero?
493
+ end
494
+
340
495
  private def valid_string?(rules, value)
341
496
  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)
497
+ return false if rules.max_length && length > rules.max_length
498
+ return false if rules.min_length && length < rules.min_length
499
+ return false if rules.pattern && !ecma_regexp(rules.pattern).match?(value)
500
+ if rules.format && (@validate_format || rules.format_assertion)
346
501
  return false unless rules.format.call(value)
347
502
  end
348
- return valid_content?(rules, value) if @validate_content
503
+ if @validate_content && (rules.decode_base64 || rules.parse_json)
504
+ return valid_content?(rules, value)
505
+ end
349
506
 
350
507
  true
351
508
  rescue RegexpError, IPAddr::InvalidAddressError
@@ -354,27 +511,23 @@ module Schemurai
354
511
 
355
512
  private def check_string(rules, value)
356
513
  length = value.length
357
- if rules.has_max_length && length > rules.max_length
514
+ if rules.max_length && length > rules.max_length
358
515
  add_error("maxLength") { Internal::ErrorMessage.size("maxLength", rules.max_length, length) }
359
516
  end
360
- if rules.has_min_length && length < rules.min_length
517
+ if rules.min_length && length < rules.min_length
361
518
  add_error("minLength") { Internal::ErrorMessage.size("minLength", rules.min_length, length) }
362
519
  end
363
- if rules.has_pattern && !ecma_regexp(rules.pattern).match?(value)
520
+ if rules.pattern && !ecma_regexp(rules.pattern).match?(value)
364
521
  add_error("pattern") { Internal::ErrorMessage.pattern(rules.pattern) }
365
522
  end
366
- if format_asserted?(rules) && !rules.format.call(value)
523
+ if rules.format && (@validate_format || rules.format_assertion) && !rules.format.call(value)
367
524
  add_error("format") { Internal::ErrorMessage.format(rules.format.name) }
368
525
  end
369
- check_content(rules, value) if @validate_content
526
+ check_content(rules, value) if @validate_content && (rules.decode_base64 || rules.parse_json)
370
527
  rescue RegexpError
371
528
  add_error("pattern") { Internal::ErrorMessage.invalid_pattern(rules.pattern) }
372
529
  end
373
530
 
374
- private def format_asserted?(rules)
375
- (@validate_format || rules.format_assertion) && !rules.format.nil?
376
- end
377
-
378
531
  private def valid_content?(rules, value)
379
532
  decoded = rules.decode_base64 ? Base64.strict_decode64(value) : value
380
533
  JSON.parse(decoded) if rules.parse_json
@@ -397,11 +550,17 @@ module Schemurai
397
550
 
398
551
  private def valid_array?(rules, value)
399
552
  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
553
+ return false if rules.max_items && length > rules.max_items
554
+ return false if rules.min_items && length < rules.min_items
402
555
  if rules.unique
403
- value.each_with_index do |item, index|
404
- return false if value[0...index].any? { |previous| json_equal?(previous, item) }
556
+ index = 1
557
+ while index < length
558
+ previous_index = 0
559
+ while previous_index < index
560
+ return false if json_equal?(value[previous_index], value[index])
561
+ previous_index += 1
562
+ end
563
+ index += 1
405
564
  end
406
565
  end
407
566
 
@@ -419,14 +578,18 @@ module Schemurai
419
578
  return false unless evaluate_valid(child, value[index])
420
579
  end
421
580
  if length > items.length && (additional = rules.additional)
422
- (items.length...length).each do |index|
581
+ index = items.length
582
+ while index < length
423
583
  return false unless evaluate_valid(additional, value[index])
584
+ index += 1
424
585
  end
425
586
  end
426
587
  elsif items
427
588
  start = rules.prefix_items&.length || 0
428
- (start...length).each do |index|
589
+ index = start
590
+ while index < length
429
591
  return false unless evaluate_valid(items, value[index])
592
+ index += 1
430
593
  end
431
594
  end
432
595
 
@@ -442,25 +605,44 @@ module Schemurai
442
605
  end
443
606
 
444
607
  private def check_array(rules, value, prior_evaluation)
445
- evaluated = []
446
- if rules.has_max_items && value.length > rules.max_items
608
+ evaluation = nil
609
+ if rules.max_items && value.length > rules.max_items
610
+ return Evaluation.invalid unless @errors
611
+
447
612
  add_error("maxItems") { Internal::ErrorMessage.size("maxItems", rules.max_items, value.length) }
448
613
  end
449
- if rules.has_min_items && value.length < rules.min_items
614
+ if rules.min_items && value.length < rules.min_items
615
+ return Evaluation.invalid unless @errors
616
+
450
617
  add_error("minItems") { Internal::ErrorMessage.size("minItems", rules.min_items, value.length) }
451
618
  end
452
619
  if rules.unique
453
- duplicate = value.each_with_index.any? do |item, index|
454
- value[0...index].any? { |previous| json_equal?(previous, item) }
620
+ duplicate = false
621
+ index = 1
622
+ while index < value.length && !duplicate
623
+ previous_index = 0
624
+ while previous_index < index
625
+ if json_equal?(value[previous_index], value[index])
626
+ duplicate = true
627
+ break
628
+ end
629
+ previous_index += 1
630
+ end
631
+ index += 1
632
+ end
633
+ if duplicate
634
+ return Evaluation.invalid unless @errors
635
+
636
+ add_error("uniqueItems") { Internal::ErrorMessage.unique_items }
455
637
  end
456
- add_error("uniqueItems") { Internal::ErrorMessage.unique_items } if duplicate
457
638
  end
458
639
 
459
640
  if (prefix_items = rules.prefix_items)
460
641
  prefix_items.each_with_index do |child, index|
461
642
  break if index >= value.length
462
- evaluate_at(child, value[index], index, "prefixItems", index)
463
- evaluated << index
643
+ valid = evaluate_child_at(child, value[index], index, "prefixItems", index)
644
+ return Evaluation.invalid if !valid && !@errors
645
+ (evaluation ||= tracked_evaluation).record_item(index)
464
646
  end
465
647
  end
466
648
 
@@ -468,107 +650,144 @@ module Schemurai
468
650
  if rules.items_list
469
651
  items.each_with_index do |child, index|
470
652
  break if index >= value.length
471
- evaluate_at(child, value[index], index, "items", index)
472
- evaluated << index
653
+ valid = evaluate_child_at(child, value[index], index, "items", index)
654
+ return Evaluation.invalid if !valid && !@errors
655
+ (evaluation ||= tracked_evaluation).record_item(index)
473
656
  end
474
657
  if value.length > items.length && (additional = rules.additional)
475
658
  (items.length...value.length).each do |index|
476
- evaluate_at(additional, value[index], index, "additionalItems")
477
- evaluated << index
659
+ valid = evaluate_child_at(additional, value[index], index, "additionalItems")
660
+ return Evaluation.invalid if !valid && !@errors
661
+ (evaluation ||= tracked_evaluation).record_item(index)
478
662
  end
479
663
  end
480
664
  elsif items
481
665
  start = rules.prefix_items&.length || 0
482
666
  (start...value.length).each do |index|
483
- evaluate_at(items, value[index], index, "items")
484
- evaluated << index
667
+ valid = evaluate_child_at(items, value[index], index, "items")
668
+ return Evaluation.invalid if !valid && !@errors
669
+ (evaluation ||= tracked_evaluation).record_item(index)
485
670
  end
486
671
  end
487
672
 
488
673
  if (contains = rules.contains)
489
- matched = value.each_index.select do |index|
490
- trial_at(contains, value[index], index, "contains").valid?
674
+ matched = nil
675
+ index = 0
676
+ while index < value.length
677
+ (matched ||= []) << index if trial_at(contains, value[index], index, "contains").valid?
678
+ index += 1
491
679
  end
492
- unless matched.length.between?(rules.min_contains, rules.max_contains)
680
+ matched_count = matched ? matched.length : 0
681
+ unless matched_count.between?(rules.min_contains, rules.max_contains)
682
+ return Evaluation.invalid unless @errors
683
+
493
684
  add_error("contains") do
494
- Internal::ErrorMessage.contains(matched.length, rules.min_contains, rules.max_contains)
685
+ Internal::ErrorMessage.contains(matched_count, rules.min_contains, rules.max_contains)
495
686
  end
496
687
  end
497
- evaluated.concat(matched)
688
+ if matched
689
+ matched.each { |index| (evaluation ||= tracked_evaluation).record_item(index) }
690
+ end
498
691
  end
499
692
 
500
- combined = prior_evaluation.evaluated_items | evaluated
501
693
  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
694
+ prior_items = prior_evaluation.evaluated_items
695
+ index = 0
696
+ while index < value.length
697
+ if prior_items.include?(index) || evaluation&.evaluated_items&.include?(index)
698
+ index += 1
699
+ next
700
+ end
701
+ valid = evaluate_child_at(unevaluated, value[index], index, "unevaluatedItems")
702
+ return Evaluation.invalid if !valid && !@errors
703
+ (evaluation ||= tracked_evaluation).record_item(index)
704
+ index += 1
505
705
  end
506
706
  end
507
- Evaluation.valid(evaluated_items: evaluated.uniq)
707
+ return Evaluation.valid unless evaluation
708
+
709
+ evaluation
508
710
  end
509
711
 
510
712
  private def valid_object?(rules, value)
511
713
  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) }
714
+ return false if rules.max_properties && length > rules.max_properties
715
+ return false if rules.min_properties && length < rules.min_properties
716
+ if rules.required && !rules.required.all? { |name| value.key?(name) }
515
717
  return false
516
718
  end
517
719
 
518
720
  properties = rules.properties
519
721
  patterns = rules.patterns
520
722
  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)
723
+ unless properties.empty? && patterns.nil? && additional.nil?
724
+ value.each do |name, property_value|
725
+ matched = false
726
+ if (child = properties[name])
727
+ matched = true
728
+ return false unless evaluate_valid(child, property_value)
729
+ end
730
+ if patterns
731
+ patterns.each do |pattern, child|
732
+ next unless ecma_regexp(pattern).match?(name)
733
+ matched = true
734
+ return false unless evaluate_valid(child, property_value)
735
+ end
736
+ end
737
+ return false if !matched && additional && !evaluate_valid(additional, property_value)
531
738
  end
532
- return false if !matched && additional && !evaluate_valid(additional, property_value)
533
739
  end
534
740
 
535
741
  if (property_names = rules.property_names)
536
742
  value.each_key { |name| return false unless evaluate_valid(property_names, name) }
537
743
  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)
744
+ if (dependencies = rules.dependencies)
745
+ dependencies.each do |name, dependency|
746
+ next unless value.key?(name)
747
+ if dependency.is_a?(Array)
748
+ return false unless dependency.all? { |required_name| value.key?(required_name) }
749
+ else
750
+ return false unless evaluate_valid(dependency, value)
751
+ end
544
752
  end
545
753
  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) }
754
+ if (dependent_required = rules.dependent_required)
755
+ dependent_required.each do |name, required_names|
756
+ next unless value.key?(name)
757
+ return false unless required_names.all? { |required_name| value.key?(required_name) }
758
+ end
549
759
  end
550
- rules.dependent_schemas.each do |name, child|
551
- next unless value.key?(name)
552
- return false unless evaluate_valid(child, value)
760
+ if (dependent_schemas = rules.dependent_schemas)
761
+ dependent_schemas.each do |name, child|
762
+ next unless value.key?(name)
763
+ return false unless evaluate_valid(child, value)
764
+ end
553
765
  end
554
766
  true
555
767
  end
556
768
 
557
769
  private def check_object(rules, value, prior_evaluation)
558
- evaluated = []
559
- if rules.has_max_properties && value.length > rules.max_properties
770
+ evaluation = nil
771
+ if rules.max_properties && value.length > rules.max_properties
772
+ return Evaluation.invalid unless @errors
773
+
560
774
  add_error("maxProperties") do
561
775
  Internal::ErrorMessage.size("maxProperties", rules.max_properties, value.length)
562
776
  end
563
777
  end
564
- if rules.has_min_properties && value.length < rules.min_properties
778
+ if rules.min_properties && value.length < rules.min_properties
779
+ return Evaluation.invalid unless @errors
780
+
565
781
  add_error("minProperties") do
566
782
  Internal::ErrorMessage.size("minProperties", rules.min_properties, value.length)
567
783
  end
568
784
  end
569
785
  if (required = rules.required)
570
786
  required.each do |name|
571
- add_error("required") { Internal::ErrorMessage.required(name) } unless value.key?(name)
787
+ next if value.key?(name)
788
+ return Evaluation.invalid unless @errors
789
+
790
+ add_error("required") { Internal::ErrorMessage.required(name) }
572
791
  end
573
792
  end
574
793
 
@@ -578,96 +797,138 @@ module Schemurai
578
797
  matched = false
579
798
  if (child = properties[name])
580
799
  matched = true
581
- evaluate_at(child, property_value, name, "properties", name)
582
- evaluated << name
800
+ valid = evaluate_child_at(child, property_value, name, "properties", name)
801
+ return Evaluation.invalid if !valid && !@errors
802
+ (evaluation ||= tracked_evaluation).record_property(name)
583
803
  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
804
+ if patterns
805
+ patterns.each do |pattern, child|
806
+ next unless ecma_regexp(pattern).match?(name)
807
+ matched = true
808
+ valid = evaluate_child_at(child, property_value, name, "patternProperties", pattern)
809
+ return Evaluation.invalid if !valid && !@errors
810
+ (evaluation ||= tracked_evaluation).record_property(name)
811
+ end
589
812
  end
590
813
  if !matched && (additional = rules.additional)
591
- evaluate_at(additional, property_value, name, "additionalProperties")
592
- evaluated << name
814
+ valid = evaluate_child_at(additional, property_value, name, "additionalProperties")
815
+ return Evaluation.invalid if !valid && !@errors
816
+ (evaluation ||= tracked_evaluation).record_property(name)
593
817
  end
594
818
  end
595
819
 
596
820
  if (property_names = rules.property_names)
597
- value.each_key { |name| evaluate_at(property_names, name, name, "propertyNames") }
821
+ value.each_key do |name|
822
+ valid = evaluate_child_at(property_names, name, name, "propertyNames")
823
+ return Evaluation.invalid if !valid && !@errors
824
+ end
598
825
  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) }
826
+ if (dependencies = rules.dependencies)
827
+ dependencies.each do |name, dependency|
828
+ next unless value.key?(name)
829
+ if dependency.is_a?(Array)
830
+ dependency.each do |required_name|
831
+ unless value.key?(required_name)
832
+ return Evaluation.invalid unless @errors
833
+
834
+ add_error("dependencies") { Internal::ErrorMessage.dependent_required(name, required_name) }
835
+ end
836
+ end
837
+ else
838
+ result = evaluate_at(dependency, value, MISSING_SEGMENT, "dependencies", name)
839
+ if result.valid? && !result.evaluated_properties.empty?
840
+ result.evaluated_properties.each do |property|
841
+ (evaluation ||= tracked_evaluation).record_property(property)
842
+ end
605
843
  end
606
844
  end
607
- else
608
- result = evaluate_at(dependency, value, MISSING_SEGMENT, "dependencies", name)
609
- evaluated.concat(result.evaluated_properties) if result.valid?
610
845
  end
611
846
  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) }
847
+ if (dependent_required = rules.dependent_required)
848
+ dependent_required.each do |name, required_names|
849
+ next unless value.key?(name)
850
+ required_names.each do |required_name|
851
+ unless value.key?(required_name)
852
+ return Evaluation.invalid unless @errors
853
+
854
+ add_error("dependentRequired") { Internal::ErrorMessage.dependent_required(name, required_name) }
855
+ end
617
856
  end
618
857
  end
619
858
  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?
859
+ if (dependent_schemas = rules.dependent_schemas)
860
+ dependent_schemas.each do |name, child|
861
+ next unless value.key?(name)
862
+ result = evaluate_at(child, value, MISSING_SEGMENT, "dependentSchemas", name)
863
+ if result.valid? && !result.evaluated_properties.empty?
864
+ result.evaluated_properties.each do |property|
865
+ (evaluation ||= tracked_evaluation).record_property(property)
866
+ end
867
+ end
868
+ end
624
869
  end
625
-
626
- combined = prior_evaluation.evaluated_properties | evaluated
627
870
  if (unevaluated = rules.unevaluated)
628
- (value.keys - combined).each do |name|
629
- evaluate_at(unevaluated, value[name], name, "unevaluatedProperties")
630
- evaluated << name
871
+ prior_properties = prior_evaluation.evaluated_properties
872
+ value.each_key do |name|
873
+ next if prior_properties.include?(name) || evaluation&.evaluated_properties&.include?(name)
874
+
875
+ valid = evaluate_child_at(unevaluated, value[name], name, "unevaluatedProperties")
876
+ return Evaluation.invalid if !valid && !@errors
877
+ (evaluation ||= tracked_evaluation).record_property(name)
631
878
  end
632
879
  end
633
- Evaluation.valid(evaluated_properties: evaluated.uniq)
880
+ return Evaluation.valid unless evaluation
881
+
882
+ evaluation
634
883
  end
635
884
 
636
885
  private def check_combiner(opcode, operand, value)
637
886
  evaluation = Evaluation.valid
638
887
  case opcode
639
888
  when :allOf
640
- operand.each_with_index do |child, index|
641
- evaluation = evaluation.merge(evaluate_at(child, value, MISSING_SEGMENT, "allOf", index))
889
+ index = 0
890
+ while index < operand.length
891
+ evaluation = evaluation.merge(evaluate_at(operand[index], value, MISSING_SEGMENT, "allOf", index))
892
+ index += 1
642
893
  end
643
894
  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) }
895
+ matches = 0
896
+ index = 0
897
+ while index < operand.length
898
+ result = trial_at(operand[index], value, MISSING_SEGMENT, "anyOf", index)
899
+ if result.valid?
900
+ matches += 1
901
+ evaluation = evaluation.merge(result)
902
+ end
903
+ index += 1
652
904
  end
905
+ add_error("anyOf") { Internal::ErrorMessage.any_of } if matches.zero?
653
906
  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?
907
+ matches = 0
908
+ matched_evaluation = nil
909
+ index = 0
910
+ while index < operand.length
911
+ result = trial_at(operand[index], value, MISSING_SEGMENT, "oneOf", index)
912
+ if result.valid?
913
+ matches += 1
914
+ matched_evaluation = result
915
+ end
916
+ index += 1
657
917
  end
658
- if matches.length == 1
659
- evaluation = evaluation.merge(matches.first)
918
+ if matches == 1
919
+ evaluation = evaluation.merge(matched_evaluation)
660
920
  else
661
- add_error("oneOf") { Internal::ErrorMessage.one_of(matches.length) }
921
+ add_error("oneOf") { Internal::ErrorMessage.one_of(matches) }
662
922
  end
663
923
  when :not
664
924
  add_error("not") { Internal::ErrorMessage.not } if trial_at(operand, value, MISSING_SEGMENT, "not").valid?
665
925
  when :conditional
666
926
  condition = trial_at(operand.condition, value, MISSING_SEGMENT, "if")
667
- evaluation = evaluation.merge(condition) if condition.valid?
668
- if condition.valid? && operand.has_then
927
+ condition_valid = condition.valid?
928
+ evaluation = evaluation.merge(condition) if condition_valid
929
+ if condition_valid && operand.then_branch
669
930
  evaluation = evaluation.merge(evaluate_at(operand.then_branch, value, MISSING_SEGMENT, "then"))
670
- elsif !condition.valid? && operand.has_else
931
+ elsif !condition_valid && operand.else_branch
671
932
  evaluation = evaluation.merge(evaluate_at(operand.else_branch, value, MISSING_SEGMENT, "else"))
672
933
  end
673
934
  end
@@ -686,6 +947,20 @@ module Schemurai
686
947
  end
687
948
 
688
949
  private def evaluate_at(program, instance, instance_segment, schema_segment, child_segment = MISSING_SEGMENT)
950
+ return evaluate(program, instance) unless @instance_path
951
+
952
+ evaluate_at_with_path(program, instance, instance_segment, schema_segment, child_segment)
953
+ end
954
+
955
+ private def evaluate_child_at(program, instance, instance_segment, schema_segment, child_segment = MISSING_SEGMENT)
956
+ return evaluate_at(program, instance, instance_segment, schema_segment, child_segment).valid? if @errors
957
+
958
+ valid = evaluate_valid(program, instance)
959
+ @error_count += 1 unless valid
960
+ valid
961
+ end
962
+
963
+ private def evaluate_at_with_path(program, instance, instance_segment, schema_segment, child_segment)
689
964
  @instance_path << instance_segment unless instance_segment.equal?(MISSING_SEGMENT)
690
965
  @schema_path << schema_segment
691
966
  @schema_path << child_segment unless child_segment.equal?(MISSING_SEGMENT)
@@ -697,6 +972,12 @@ module Schemurai
697
972
  end
698
973
 
699
974
  private def trial_at(program, instance, instance_segment, schema_segment, child_segment = MISSING_SEGMENT)
975
+ return trial(program, instance) unless @instance_path
976
+
977
+ trial_at_with_path(program, instance, instance_segment, schema_segment, child_segment)
978
+ end
979
+
980
+ private def trial_at_with_path(program, instance, instance_segment, schema_segment, child_segment)
700
981
  @instance_path << instance_segment unless instance_segment.equal?(MISSING_SEGMENT)
701
982
  @schema_path << schema_segment
702
983
  @schema_path << child_segment unless child_segment.equal?(MISSING_SEGMENT)
@@ -726,30 +1007,38 @@ module Schemurai
726
1007
  end
727
1008
 
728
1009
  private def json_equal?(left, right)
729
- return false if json_kind(left) != json_kind(right)
1010
+ if left.is_a?(Numeric) && !left.is_a?(Complex)
1011
+ return right.is_a?(Numeric) && !right.is_a?(Complex) && left == right
1012
+ end
1013
+ return false unless left.instance_of?(right.class)
1014
+
730
1015
  case left
731
1016
  when Hash
732
1017
  left.length == right.length && left.all? do |key, value|
733
1018
  right.key?(key) && json_equal?(value, right[key])
734
1019
  end
735
1020
  when Array
736
- left.length == right.length && left.each_index.all? { |index| json_equal?(left[index], right[index]) }
1021
+ return false unless left.length == right.length
1022
+
1023
+ index = 0
1024
+ while index < left.length
1025
+ return false unless json_equal?(left[index], right[index])
1026
+ index += 1
1027
+ end
1028
+ true
737
1029
  else
738
1030
  left == right
739
1031
  end
740
1032
  end
741
1033
 
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
1034
  private def decimal(value)
750
1035
  return value if value.is_a?(Integer) || value.is_a?(Rational)
751
1036
 
752
- Rational(value.to_s)
1037
+ decimals = (@decimals ||= {})
1038
+ return decimals[value] if decimals.key?(value)
1039
+
1040
+ decimals.clear if decimals.length >= DECIMAL_CACHE_LIMIT
1041
+ decimals[value] = Rational(value.to_s)
753
1042
  end
754
1043
 
755
1044
  private def ecma_regexp(pattern)
@@ -803,7 +1092,7 @@ module Schemurai
803
1092
  pointer << "/" << segment.to_s.gsub("~", "~0").gsub("/", "~1")
804
1093
  end
805
1094
 
806
- private_constant :MISSING_SEGMENT
1095
+ private_constant :DECIMAL_CACHE_LIMIT, :MISSING_SEGMENT
807
1096
  end
808
1097
  end
809
1098