schemurai 2.0.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.
@@ -3,12 +3,54 @@
3
3
  require "base64"
4
4
  require "json"
5
5
  require_relative "../evaluation"
6
+ require_relative "../error_message"
6
7
  require_relative "compiler"
7
8
 
8
9
  module Schemurai
9
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
+
10
51
  class Evaluator
11
52
  MISSING_SEGMENT = Object.new.freeze
53
+ DECIMAL_CACHE_LIMIT = 16
12
54
 
13
55
  def backend = :vm
14
56
 
@@ -20,6 +62,14 @@ module Schemurai
20
62
  @validate_format = format
21
63
  @regexps = nil
22
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
23
73
  end
24
74
 
25
75
  def validate(instance)
@@ -33,31 +83,52 @@ module Schemurai
33
83
 
34
84
  def valid?(instance)
35
85
  @errors = nil
36
- 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
37
91
  evaluate_valid(@root, instance)
38
92
  end
39
93
 
40
94
  private def prepare_evaluation(paths:)
41
95
  @error_count = 0
42
- @dynamic_scope = nil
43
- @instance_path = paths ? [] : nil
44
- @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
45
107
  end
46
108
 
47
109
  private def evaluate_valid(program, instance)
48
- if program.tracks_evaluation?
49
- @instance_path ||= []
50
- @schema_path ||= []
51
- 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
52
123
  end
53
-
54
- entered_scope = enter_scope(program)
55
- program.code.each do |opcode, operand, extra|
124
+ instruction_index = 1
125
+ while (opcode = code[instruction_index])
126
+ operand = code[instruction_index + 1]
56
127
  case opcode
57
128
  when :boolean
58
129
  return operand
59
130
  when :ref
60
- target = @compiler.resolve(program, operand.value)
131
+ target = reference_target(program, operand)
61
132
  return false unless valid_reference?(program, target, instance)
62
133
  when :recursive_ref
63
134
  target = recursive_target(program, operand)
@@ -74,11 +145,22 @@ module Schemurai
74
145
  when :type_array
75
146
  return false unless instance.is_a?(Array)
76
147
  when :type_number
77
- return false unless number?(instance)
148
+ return false unless instance.is_a?(Numeric) && !instance.is_a?(Complex)
78
149
  when :type_integer
79
- return false unless integer?(instance)
150
+ return false unless instance.is_a?(Numeric) && !instance.is_a?(Complex) && instance.finite? && instance.to_i == instance
80
151
  when :type_string
81
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)
82
164
  when :types
83
165
  return false if (operand.mask & instance_type_mask(instance, integer: (operand.mask & TYPE_INTEGER) != 0)).zero?
84
166
  when :enum
@@ -100,8 +182,8 @@ module Schemurai
100
182
  return false if evaluate_valid(operand, instance)
101
183
  when :conditional
102
184
  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)
185
+ return false if operand.then_branch && !evaluate_valid(operand.then_branch, instance)
186
+ elsif operand.else_branch && !evaluate_valid(operand.else_branch, instance)
105
187
  return false
106
188
  end
107
189
  when :number
@@ -115,6 +197,7 @@ module Schemurai
115
197
  else
116
198
  raise "unknown VM instruction #{opcode.inspect}"
117
199
  end
200
+ instruction_index += 2
118
201
  end
119
202
  true
120
203
  rescue ResolutionError
@@ -123,16 +206,38 @@ module Schemurai
123
206
  leave_scope if entered_scope
124
207
  end
125
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
+
126
220
  private def evaluate(program, instance)
127
221
  before = @error_count
128
222
  evaluation = Evaluation.valid
129
- 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
130
233
 
131
- program.code.each do |opcode, operand, extra|
234
+ instruction_index = 1
235
+ while (opcode = code[instruction_index])
236
+ operand = code[instruction_index + 1]
132
237
  case opcode
133
238
  when :boolean
134
239
  if operand == false
135
- add_error("falseSchema", "boolean schema is false", append_keyword: false)
240
+ add_error("falseSchema", append_keyword: false) { Internal::ErrorMessage.false_schema }
136
241
  evaluation = Evaluation.invalid
137
242
  end
138
243
  when :ref
@@ -142,70 +247,108 @@ module Schemurai
142
247
  when :dynamic_ref
143
248
  evaluation = evaluation.merge(evaluate_dynamic_ref(program, operand, instance))
144
249
  when :type_null
145
- check_compiled_type("null", instance.nil?)
250
+ check_compiled_type("null", instance.nil?, instance)
146
251
  when :type_boolean
147
- check_compiled_type("boolean", instance == true || instance == false)
252
+ check_compiled_type("boolean", instance == true || instance == false, instance)
148
253
  when :type_object
149
- check_compiled_type("object", instance.is_a?(Hash))
254
+ check_compiled_type("object", instance.is_a?(Hash), instance)
150
255
  when :type_array
151
- check_compiled_type("array", instance.is_a?(Array))
256
+ check_compiled_type("array", instance.is_a?(Array), instance)
152
257
  when :type_number
153
- check_compiled_type("number", number?(instance))
258
+ check_compiled_type("number", number?(instance), instance)
154
259
  when :type_integer
155
- check_compiled_type("integer", integer?(instance))
260
+ check_compiled_type("integer", integer?(instance), instance)
156
261
  when :type_string
157
- check_compiled_type("string", instance.is_a?(String))
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
158
299
  when :types
159
300
  check_compiled_types(operand, instance)
160
301
  when :enum
161
- add_error("enum", "value is not in enum") unless operand.any? { |candidate| json_equal?(candidate, instance) }
302
+ add_error("enum") { Internal::ErrorMessage.enum } unless operand.any? { |candidate| json_equal?(candidate, instance) }
162
303
  when :const
163
- add_error("const", "value does not equal const") unless json_equal?(operand, instance)
304
+ add_error("const") { Internal::ErrorMessage.const } unless json_equal?(operand, instance)
164
305
  when :allOf, :anyOf, :oneOf, :not, :conditional
165
- 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)
166
309
  when :number
167
310
  check_number(operand, instance) if instance.is_a?(Numeric) && !instance.is_a?(Complex)
168
311
  when :string
169
312
  check_string(operand, instance) if instance.is_a?(String)
170
313
  when :array
171
- 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
172
319
  when :object
173
- 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
174
325
  else
175
326
  raise "unknown VM instruction #{opcode.inspect}"
176
327
  end
328
+ instruction_index += 2
177
329
  end
178
330
  (@error_count == before) ? evaluation : Evaluation.invalid
179
331
  ensure
180
332
  leave_scope if entered_scope
181
333
  end
182
334
 
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
335
  private def leave_scope
192
336
  @dynamic_scope.pop
193
337
  end
194
338
 
195
339
  private def recursive_target(program, rules)
196
- target = @compiler.resolve(program, rules.value)
340
+ target = reference_target(program, rules)
197
341
  return target unless rules.fragment == "" && target.recursive_anchor?
198
342
 
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
343
+ @dynamic_scope&.each do |resource|
344
+ compiled = @compiler.compile(resource.root)
345
+ return compiled if compiled.recursive_anchor?
346
+ end
347
+ target
205
348
  end
206
349
 
207
350
  private def dynamic_target(program, rules)
208
- target = @compiler.resolve(program, rules.value)
351
+ target = reference_target(program, rules)
209
352
  fragment = rules.fragment
210
353
  return target if fragment.nil? || fragment.empty? || fragment.start_with?("/")
211
354
 
@@ -220,18 +363,22 @@ module Schemurai
220
363
 
221
364
  private def valid_reference?(source, target, instance)
222
365
  instances = active_instances(source)
223
- instance_id = instance.object_id
224
- return true if instances[instance_id]
366
+ return true if instances.key?(instance)
225
367
 
226
- instances[instance_id] = true
368
+ instances[instance] = true
227
369
  activated = true
228
370
  evaluate_valid(target, instance)
229
371
  ensure
230
- 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) }
231
378
  end
232
379
 
233
380
  private def evaluate_ref(source, rules, instance)
234
- target = @compiler.resolve(source, rules.value)
381
+ target = reference_target(source, rules)
235
382
  evaluate_reference(source, target, instance, "$ref")
236
383
  rescue ResolutionError => error
237
384
  unresolved_reference("$ref", error)
@@ -251,14 +398,13 @@ module Schemurai
251
398
 
252
399
  private def evaluate_reference(source, target, instance, keyword)
253
400
  instances = active_instances(source)
254
- instance_id = instance.object_id
255
- return Evaluation.valid if instances[instance_id]
401
+ return Evaluation.valid if instances.key?(instance)
256
402
 
257
- instances[instance_id] = true
403
+ instances[instance] = true
258
404
  activated = true
259
405
  evaluate_at(target, instance, MISSING_SEGMENT, keyword)
260
406
  ensure
261
- instances&.delete(instance_id) if activated
407
+ instances&.delete(instance) if activated
262
408
  end
263
409
 
264
410
  private def unresolved_reference(keyword, error)
@@ -267,19 +413,19 @@ module Schemurai
267
413
  end
268
414
 
269
415
  private def active_instances(program)
270
- active = (@active ||= {})
271
- active[program.node.object_id] ||= {}
416
+ active = (@active ||= {}.compare_by_identity)
417
+ active[program] ||= {}.compare_by_identity
272
418
  end
273
419
 
274
- private def check_compiled_type(name, valid)
275
- add_error("type", "expected #{name}") unless valid
420
+ private def check_compiled_type(name, valid, value)
421
+ add_error("type") { Internal::ErrorMessage.type(name, value) } unless valid
276
422
  end
277
423
 
278
424
  private def check_compiled_types(rules, value)
279
425
  instance_mask = instance_type_mask(value, integer: (rules.mask & TYPE_INTEGER) != 0)
280
426
  return unless (rules.mask & instance_mask).zero?
281
427
 
282
- add_error("type", "expected #{rules.names.join(" or ")}")
428
+ add_error("type") { Internal::ErrorMessage.type(rules.names, value) }
283
429
  end
284
430
 
285
431
  private def instance_type_mask(value, integer:)
@@ -300,47 +446,63 @@ module Schemurai
300
446
 
301
447
  private def valid_number?(rules, value)
302
448
  mask = rules.mask
303
- actual = decimal(value)
449
+ actual = (mask & MULTIPLE_OF).zero? ? value : decimal(value)
304
450
  return false if (mask & MAXIMUM) != 0 && actual > rules.maximum
305
451
  return false if (mask & MINIMUM) != 0 && actual < rules.minimum
306
452
  return false if (mask & EXCLUSIVE_MAXIMUM) != 0 && actual >= rules.exclusive_maximum
307
453
  return false if (mask & EXCLUSIVE_MINIMUM) != 0 && actual <= rules.exclusive_minimum
308
454
  return true if (mask & MULTIPLE_OF).zero?
309
455
 
310
- rules.multiple_of.positive? && actual.remainder(rules.multiple_of).zero?
456
+ valid_multiple?(rules, value)
311
457
  end
312
458
 
313
459
  private def check_number(rules, value)
314
460
  mask = rules.mask
315
- actual = decimal(value)
461
+ actual = (mask & MULTIPLE_OF).zero? ? value : decimal(value)
316
462
  if (mask & MAXIMUM) != 0 && actual > rules.maximum
317
- add_error("maximum", "numeric limit was exceeded")
463
+ add_error("maximum") { Internal::ErrorMessage.numeric_limit("maximum", rules.maximum) }
318
464
  end
319
465
  if (mask & MINIMUM) != 0 && actual < rules.minimum
320
- add_error("minimum", "numeric limit was exceeded")
466
+ add_error("minimum") { Internal::ErrorMessage.numeric_limit("minimum", rules.minimum) }
321
467
  end
322
468
  if (mask & EXCLUSIVE_MAXIMUM) != 0 && actual >= rules.exclusive_maximum
323
- add_error("exclusiveMaximum", "numeric limit was exceeded")
469
+ add_error("exclusiveMaximum") do
470
+ Internal::ErrorMessage.numeric_limit("exclusiveMaximum", rules.exclusive_maximum)
471
+ end
324
472
  end
325
473
  if (mask & EXCLUSIVE_MINIMUM) != 0 && actual <= rules.exclusive_minimum
326
- add_error("exclusiveMinimum", "numeric limit was exceeded")
474
+ add_error("exclusiveMinimum") do
475
+ Internal::ErrorMessage.numeric_limit("exclusiveMinimum", rules.exclusive_minimum)
476
+ end
327
477
  end
328
478
  return if (mask & MULTIPLE_OF).zero?
329
479
 
330
480
  divisor = rules.multiple_of
331
- valid = divisor.positive? && actual.remainder(divisor).zero?
332
- add_error("multipleOf", "number is not a multiple") unless valid
481
+ valid = valid_multiple?(rules, value)
482
+ add_error("multipleOf") { Internal::ErrorMessage.multiple_of(divisor) } unless valid
483
+ end
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?
333
493
  end
334
494
 
335
495
  private def valid_string?(rules, value)
336
496
  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)
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)
341
501
  return false unless rules.format.call(value)
342
502
  end
343
- 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
344
506
 
345
507
  true
346
508
  rescue RegexpError, IPAddr::InvalidAddressError
@@ -349,25 +511,21 @@ module Schemurai
349
511
 
350
512
  private def check_string(rules, value)
351
513
  length = value.length
352
- if rules.has_max_length && length > rules.max_length
353
- add_error("maxLength", "size limit was exceeded")
514
+ if rules.max_length && length > rules.max_length
515
+ add_error("maxLength") { Internal::ErrorMessage.size("maxLength", rules.max_length, length) }
354
516
  end
355
- if rules.has_min_length && length < rules.min_length
356
- add_error("minLength", "size limit was exceeded")
517
+ if rules.min_length && length < rules.min_length
518
+ add_error("minLength") { Internal::ErrorMessage.size("minLength", rules.min_length, length) }
357
519
  end
358
- if rules.has_pattern && !ecma_regexp(rules.pattern).match?(value)
359
- add_error("pattern", "string does not match pattern")
520
+ if rules.pattern && !ecma_regexp(rules.pattern).match?(value)
521
+ add_error("pattern") { Internal::ErrorMessage.pattern(rules.pattern) }
360
522
  end
361
- if format_asserted?(rules) && !rules.format.call(value)
362
- add_error("format", "string is not a valid #{rules.format.name}")
523
+ if rules.format && (@validate_format || rules.format_assertion) && !rules.format.call(value)
524
+ add_error("format") { Internal::ErrorMessage.format(rules.format.name) }
363
525
  end
364
- check_content(rules, value) if @validate_content
526
+ check_content(rules, value) if @validate_content && (rules.decode_base64 || rules.parse_json)
365
527
  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?
528
+ add_error("pattern") { Internal::ErrorMessage.invalid_pattern(rules.pattern) }
371
529
  end
372
530
 
373
531
  private def valid_content?(rules, value)
@@ -385,16 +543,24 @@ module Schemurai
385
543
  JSON.parse(decoded)
386
544
  rescue ArgumentError, JSON::ParserError
387
545
  keyword = rules.decode_base64 ? "contentEncoding" : "contentMediaType"
388
- add_error(keyword, "string content is invalid")
546
+ add_error(keyword) do
547
+ (keyword == "contentEncoding") ? Internal::ErrorMessage.content_encoding : Internal::ErrorMessage.content_media_type
548
+ end
389
549
  end
390
550
 
391
551
  private def valid_array?(rules, value)
392
552
  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
553
+ return false if rules.max_items && length > rules.max_items
554
+ return false if rules.min_items && length < rules.min_items
395
555
  if rules.unique
396
- value.each_with_index do |item, index|
397
- 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
398
564
  end
399
565
  end
400
566
 
@@ -412,14 +578,18 @@ module Schemurai
412
578
  return false unless evaluate_valid(child, value[index])
413
579
  end
414
580
  if length > items.length && (additional = rules.additional)
415
- (items.length...length).each do |index|
581
+ index = items.length
582
+ while index < length
416
583
  return false unless evaluate_valid(additional, value[index])
584
+ index += 1
417
585
  end
418
586
  end
419
587
  elsif items
420
588
  start = rules.prefix_items&.length || 0
421
- (start...length).each do |index|
589
+ index = start
590
+ while index < length
422
591
  return false unless evaluate_valid(items, value[index])
592
+ index += 1
423
593
  end
424
594
  end
425
595
 
@@ -435,25 +605,44 @@ module Schemurai
435
605
  end
436
606
 
437
607
  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")
608
+ evaluation = nil
609
+ if rules.max_items && value.length > rules.max_items
610
+ return Evaluation.invalid unless @errors
611
+
612
+ add_error("maxItems") { Internal::ErrorMessage.size("maxItems", rules.max_items, value.length) }
441
613
  end
442
- if rules.has_min_items && value.length < rules.min_items
443
- add_error("minItems", "size limit was exceeded")
614
+ if rules.min_items && value.length < rules.min_items
615
+ return Evaluation.invalid unless @errors
616
+
617
+ add_error("minItems") { Internal::ErrorMessage.size("minItems", rules.min_items, value.length) }
444
618
  end
445
619
  if rules.unique
446
- duplicate = value.each_with_index.any? do |item, index|
447
- 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 }
448
637
  end
449
- add_error("uniqueItems", "array items are not unique") if duplicate
450
638
  end
451
639
 
452
640
  if (prefix_items = rules.prefix_items)
453
641
  prefix_items.each_with_index do |child, index|
454
642
  break if index >= value.length
455
- evaluate_at(child, value[index], index, "prefixItems", index)
456
- 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)
457
646
  end
458
647
  end
459
648
 
@@ -461,101 +650,144 @@ module Schemurai
461
650
  if rules.items_list
462
651
  items.each_with_index do |child, index|
463
652
  break if index >= value.length
464
- evaluate_at(child, value[index], index, "items", index)
465
- 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)
466
656
  end
467
657
  if value.length > items.length && (additional = rules.additional)
468
658
  (items.length...value.length).each do |index|
469
- evaluate_at(additional, value[index], index, "additionalItems")
470
- 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)
471
662
  end
472
663
  end
473
664
  elsif items
474
665
  start = rules.prefix_items&.length || 0
475
666
  (start...value.length).each do |index|
476
- evaluate_at(items, value[index], index, "items")
477
- 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)
478
670
  end
479
671
  end
480
672
 
481
673
  if (contains = rules.contains)
482
- matched = value.each_index.select do |index|
483
- 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
484
679
  end
485
- unless matched.length.between?(rules.min_contains, rules.max_contains)
486
- add_error("contains", "matched #{matched.length} array items")
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
+
684
+ add_error("contains") do
685
+ Internal::ErrorMessage.contains(matched_count, rules.min_contains, rules.max_contains)
686
+ end
687
+ end
688
+ if matched
689
+ matched.each { |index| (evaluation ||= tracked_evaluation).record_item(index) }
487
690
  end
488
- evaluated.concat(matched)
489
691
  end
490
692
 
491
- combined = prior_evaluation.evaluated_items | evaluated
492
693
  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
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
496
705
  end
497
706
  end
498
- Evaluation.valid(evaluated_items: evaluated.uniq)
707
+ return Evaluation.valid unless evaluation
708
+
709
+ evaluation
499
710
  end
500
711
 
501
712
  private def valid_object?(rules, value)
502
713
  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) }
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) }
506
717
  return false
507
718
  end
508
719
 
509
720
  properties = rules.properties
510
721
  patterns = rules.patterns
511
722
  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)
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)
522
738
  end
523
- return false if !matched && additional && !evaluate_valid(additional, property_value)
524
739
  end
525
740
 
526
741
  if (property_names = rules.property_names)
527
742
  value.each_key { |name| return false unless evaluate_valid(property_names, name) }
528
743
  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)
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
535
752
  end
536
753
  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) }
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
540
759
  end
541
- rules.dependent_schemas.each do |name, child|
542
- next unless value.key?(name)
543
- 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
544
765
  end
545
766
  true
546
767
  end
547
768
 
548
769
  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")
770
+ evaluation = nil
771
+ if rules.max_properties && value.length > rules.max_properties
772
+ return Evaluation.invalid unless @errors
773
+
774
+ add_error("maxProperties") do
775
+ Internal::ErrorMessage.size("maxProperties", rules.max_properties, value.length)
776
+ end
552
777
  end
553
- if rules.has_min_properties && value.length < rules.min_properties
554
- add_error("minProperties", "size limit was exceeded")
778
+ if rules.min_properties && value.length < rules.min_properties
779
+ return Evaluation.invalid unless @errors
780
+
781
+ add_error("minProperties") do
782
+ Internal::ErrorMessage.size("minProperties", rules.min_properties, value.length)
783
+ end
555
784
  end
556
785
  if (required = rules.required)
557
786
  required.each do |name|
558
- add_error("required", "required property #{name.inspect} is missing") 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) }
559
791
  end
560
792
  end
561
793
 
@@ -565,96 +797,138 @@ module Schemurai
565
797
  matched = false
566
798
  if (child = properties[name])
567
799
  matched = true
568
- evaluate_at(child, property_value, name, "properties", name)
569
- 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)
570
803
  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
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
576
812
  end
577
813
  if !matched && (additional = rules.additional)
578
- evaluate_at(additional, property_value, name, "additionalProperties")
579
- 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)
580
817
  end
581
818
  end
582
819
 
583
820
  if (property_names = rules.property_names)
584
- 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
585
825
  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}")
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
592
843
  end
593
844
  end
594
- else
595
- result = evaluate_at(dependency, value, MISSING_SEGMENT, "dependencies", name)
596
- evaluated.concat(result.evaluated_properties) if result.valid?
597
845
  end
598
846
  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}")
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
604
856
  end
605
857
  end
606
858
  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?
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
611
869
  end
612
-
613
- combined = prior_evaluation.evaluated_properties | evaluated
614
870
  if (unevaluated = rules.unevaluated)
615
- (value.keys - combined).each do |name|
616
- evaluate_at(unevaluated, value[name], name, "unevaluatedProperties")
617
- 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)
618
878
  end
619
879
  end
620
- Evaluation.valid(evaluated_properties: evaluated.uniq)
880
+ return Evaluation.valid unless evaluation
881
+
882
+ evaluation
621
883
  end
622
884
 
623
885
  private def check_combiner(opcode, operand, value)
624
886
  evaluation = Evaluation.valid
625
887
  case opcode
626
888
  when :allOf
627
- operand.each_with_index do |child, index|
628
- 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
629
893
  end
630
894
  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) }
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
639
904
  end
905
+ add_error("anyOf") { Internal::ErrorMessage.any_of } if matches.zero?
640
906
  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?
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
644
917
  end
645
- if matches.length == 1
646
- evaluation = evaluation.merge(matches.first)
918
+ if matches == 1
919
+ evaluation = evaluation.merge(matched_evaluation)
647
920
  else
648
- add_error("oneOf", "expected exactly one match, got #{matches.length}")
921
+ add_error("oneOf") { Internal::ErrorMessage.one_of(matches) }
649
922
  end
650
923
  when :not
651
- add_error("not", "subschema matched") if trial_at(operand, value, MISSING_SEGMENT, "not").valid?
924
+ add_error("not") { Internal::ErrorMessage.not } if trial_at(operand, value, MISSING_SEGMENT, "not").valid?
652
925
  when :conditional
653
926
  condition = trial_at(operand.condition, value, MISSING_SEGMENT, "if")
654
- evaluation = evaluation.merge(condition) if condition.valid?
655
- 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
656
930
  evaluation = evaluation.merge(evaluate_at(operand.then_branch, value, MISSING_SEGMENT, "then"))
657
- elsif !condition.valid? && operand.has_else
931
+ elsif !condition_valid && operand.else_branch
658
932
  evaluation = evaluation.merge(evaluate_at(operand.else_branch, value, MISSING_SEGMENT, "else"))
659
933
  end
660
934
  end
@@ -673,6 +947,20 @@ module Schemurai
673
947
  end
674
948
 
675
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)
676
964
  @instance_path << instance_segment unless instance_segment.equal?(MISSING_SEGMENT)
677
965
  @schema_path << schema_segment
678
966
  @schema_path << child_segment unless child_segment.equal?(MISSING_SEGMENT)
@@ -684,6 +972,12 @@ module Schemurai
684
972
  end
685
973
 
686
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)
687
981
  @instance_path << instance_segment unless instance_segment.equal?(MISSING_SEGMENT)
688
982
  @schema_path << schema_segment
689
983
  @schema_path << child_segment unless child_segment.equal?(MISSING_SEGMENT)
@@ -694,7 +988,7 @@ module Schemurai
694
988
  @instance_path.pop unless instance_segment.equal?(MISSING_SEGMENT)
695
989
  end
696
990
 
697
- private def add_error(keyword, message, append_keyword: true)
991
+ private def add_error(keyword, message = nil, append_keyword: true)
698
992
  @error_count += 1
699
993
  return false unless @errors
700
994
 
@@ -703,7 +997,7 @@ module Schemurai
703
997
  keyword: keyword,
704
998
  instance_path: pointer(@instance_path),
705
999
  schema_path: pointer(@schema_path, final_segment),
706
- message: message
1000
+ message: message || yield
707
1001
  )
708
1002
  false
709
1003
  end
@@ -713,30 +1007,38 @@ module Schemurai
713
1007
  end
714
1008
 
715
1009
  private def json_equal?(left, right)
716
- 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
+
717
1015
  case left
718
1016
  when Hash
719
1017
  left.length == right.length && left.all? do |key, value|
720
1018
  right.key?(key) && json_equal?(value, right[key])
721
1019
  end
722
1020
  when Array
723
- 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
724
1029
  else
725
1030
  left == right
726
1031
  end
727
1032
  end
728
1033
 
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
1034
  private def decimal(value)
737
1035
  return value if value.is_a?(Integer) || value.is_a?(Rational)
738
1036
 
739
- 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)
740
1042
  end
741
1043
 
742
1044
  private def ecma_regexp(pattern)
@@ -790,7 +1092,7 @@ module Schemurai
790
1092
  pointer << "/" << segment.to_s.gsub("~", "~0").gsub("/", "~1")
791
1093
  end
792
1094
 
793
- private_constant :MISSING_SEGMENT
1095
+ private_constant :DECIMAL_CACHE_LIMIT, :MISSING_SEGMENT
794
1096
  end
795
1097
  end
796
1098