schemurai 3.1.0-aarch64-linux-gnu

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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +395 -0
  4. data/lib/schemurai/backend.rb +34 -0
  5. data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
  6. data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
  7. data/lib/schemurai/container_checks.rb +534 -0
  8. data/lib/schemurai/decimal.rb +11 -0
  9. data/lib/schemurai/dialect.rb +106 -0
  10. data/lib/schemurai/dialect_keywords.rb +91 -0
  11. data/lib/schemurai/dialects/draft2019_09.rb +27 -0
  12. data/lib/schemurai/dialects/draft2020_12.rb +27 -0
  13. data/lib/schemurai/dialects/draft7.rb +27 -0
  14. data/lib/schemurai/ecma_regexp.rb +46 -0
  15. data/lib/schemurai/error_message.rb +102 -0
  16. data/lib/schemurai/evaluation.rb +63 -0
  17. data/lib/schemurai/evaluation_limits.rb +65 -0
  18. data/lib/schemurai/evaluation_pool.rb +66 -0
  19. data/lib/schemurai/evaluator.rb +861 -0
  20. data/lib/schemurai/formats.rb +403 -0
  21. data/lib/schemurai/json_pointer.rb +11 -0
  22. data/lib/schemurai/limit_budget.rb +78 -0
  23. data/lib/schemurai/limit_exceeded.rb +18 -0
  24. data/lib/schemurai/limited_evaluator.rb +43 -0
  25. data/lib/schemurai/meta_schemas/draft2019_09.rb +149 -0
  26. data/lib/schemurai/meta_schemas/draft2020_12.rb +149 -0
  27. data/lib/schemurai/meta_schemas/draft7.rb +121 -0
  28. data/lib/schemurai/meta_schemas.rb +28 -0
  29. data/lib/schemurai/native/4.0/schemurai_native.so +0 -0
  30. data/lib/schemurai/native/diagnostics.rb +53 -0
  31. data/lib/schemurai/native/evaluator.rb +87 -0
  32. data/lib/schemurai/native/null_numeric.rb +23 -0
  33. data/lib/schemurai/number_checks.rb +128 -0
  34. data/lib/schemurai/schema_domain.rb +95 -0
  35. data/lib/schemurai/schema_graph.rb +538 -0
  36. data/lib/schemurai/schema_node.rb +55 -0
  37. data/lib/schemurai/string_checks.rb +166 -0
  38. data/lib/schemurai/type_checks.rb +59 -0
  39. data/lib/schemurai/unique_items.rb +68 -0
  40. data/lib/schemurai/value_equality.rb +72 -0
  41. data/lib/schemurai/version.rb +6 -0
  42. data/lib/schemurai/vm/compiler.rb +410 -0
  43. data/lib/schemurai/vm/evaluator.rb +663 -0
  44. data/lib/schemurai/vm/limited_evaluator.rb +27 -0
  45. data/lib/schemurai.rb +307 -0
  46. data/lib/schemurai_native.rb +4 -0
  47. data/sig/schemurai.rbs +108 -0
  48. metadata +121 -0
@@ -0,0 +1,663 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../decimal"
4
+ require "base64"
5
+ require "json"
6
+ require_relative "../evaluation_pool"
7
+ require_relative "../evaluation"
8
+ require_relative "../error_message"
9
+ require_relative "../unique_items"
10
+ require_relative "../value_equality"
11
+ require_relative "../container_checks"
12
+ require_relative "../ecma_regexp"
13
+ require_relative "../json_pointer"
14
+ require_relative "compiler"
15
+ require_relative "../string_checks"
16
+
17
+ module Schemurai
18
+ module VM
19
+ EvaluationBuffer = Internal::EvaluationBuffer
20
+ private_constant :EvaluationBuffer
21
+
22
+ class Evaluator
23
+ include Internal::UniqueItems
24
+ include Internal::ValueEquality
25
+ include Internal::ContainerChecks::Bounds
26
+ include Internal::ContainerChecks::ObjectChildren
27
+ include Internal::ContainerChecks::ArrayChildren
28
+ include Internal::ContainerChecks::ArrayDiagnostics
29
+ include Internal::ContainerChecks::ObjectDiagnostics
30
+ include Internal::NumberChecks::Evaluation
31
+ include Internal::StringChecks::Evaluation
32
+ include Internal::StringChecks::Regexps
33
+
34
+ MISSING_SEGMENT = Internal::ContainerChecks::ObjectDiagnostics::MISSING_SEGMENT
35
+
36
+ def backend = :vm
37
+
38
+ def initialize(graph, compiler, root, content: false, format: false, number_state: nil, regexps: nil, dynamic_scope: nil, active_state: nil, native_references: nil, native_reports: nil)
39
+ @graph = graph
40
+ @compiler = compiler
41
+ @root = root
42
+ @validate_content = content
43
+ @validate_format = format
44
+ @regexps = regexps
45
+ @active = active_state
46
+ @native_reports = native_reports
47
+ @native_references = native_references
48
+ @resolved_references = nil
49
+ @number_state = number_state
50
+ @dynamic_scope = dynamic_scope
51
+ @instance_path_buffer = nil
52
+ @schema_path_buffer = nil
53
+ @evaluation_pool = native_reports ? native_reports[1] : Internal::EvaluationPool.new
54
+ end
55
+
56
+ def validate(instance)
57
+ @errors = []
58
+ prepare_evaluation(paths: true)
59
+ @native_reports[0] = self if @native_reports
60
+ evaluate(@root, instance)
61
+ Result.new(@errors)
62
+ ensure
63
+ @errors = nil
64
+ end
65
+
66
+ def valid?(instance)
67
+ @errors = nil
68
+ @error_count = 0
69
+ @dynamic_scope&.clear
70
+ @instance_path = nil
71
+ @schema_path = nil
72
+ reset_evaluation_pool
73
+ @native_reports[0] = self if @native_reports
74
+ evaluate_valid(@root, instance)
75
+ end
76
+
77
+ # Data exchange for retained native blocks during a compatibility traversal.
78
+ # These methods expose collection state, never programs or instructions.
79
+ private def native_report_context
80
+ [@errors, @error_count || 0, @instance_path, @schema_path]
81
+ end
82
+
83
+ private def native_clear_collection(paths = false)
84
+ @errors = nil
85
+ @error_count = 0
86
+ if paths
87
+ (@instance_path_buffer ||= []).clear
88
+ (@schema_path_buffer ||= []).clear
89
+ @instance_path = @instance_path_buffer
90
+ @schema_path = @schema_path_buffer
91
+ else
92
+ @instance_path = nil
93
+ @schema_path = nil
94
+ end
95
+ end
96
+
97
+ private def native_append_report(errors, count, reset)
98
+ native_clear_collection(reset == 2) unless reset.zero?
99
+ @error_count += count
100
+ @errors.concat(errors) if @errors
101
+ @native_reports[0] = self
102
+ end
103
+
104
+ private def prepare_evaluation(paths:)
105
+ @error_count = 0
106
+ @dynamic_scope&.clear
107
+ reset_evaluation_pool
108
+ if paths
109
+ (@instance_path_buffer ||= []).clear
110
+ (@schema_path_buffer ||= []).clear
111
+ @instance_path = @instance_path_buffer
112
+ @schema_path = @schema_path_buffer
113
+ else
114
+ @instance_path = nil
115
+ @schema_path = nil
116
+ end
117
+ end
118
+
119
+ private def evaluate_valid(program, instance)
120
+ code = program.code
121
+ flags = code[0]
122
+ entered_scope = false
123
+ unless flags.zero?
124
+ return evaluate(program, instance).valid? if (flags & TRACKS_EVALUATION) != 0
125
+
126
+ if (flags & TRACKS_DYNAMIC_SCOPE) != 0
127
+ resource = program.node.resource
128
+ unless @dynamic_scope&.last.equal?(resource)
129
+ (@dynamic_scope ||= []) << resource
130
+ entered_scope = true
131
+ end
132
+ end
133
+ end
134
+ instruction_index = 1
135
+ while (opcode = code[instruction_index])
136
+ @budget&.checkpoint
137
+ operand = code[instruction_index + 1]
138
+ case opcode
139
+ when :boolean
140
+ return operand
141
+ when :ref
142
+ target = reference_target(program, operand)
143
+ return false unless valid_reference?(program, target, instance)
144
+ when :recursive_ref
145
+ target = recursive_target(program, operand)
146
+ return false unless valid_reference?(program, target, instance)
147
+ when :dynamic_ref
148
+ target = dynamic_target(program, operand)
149
+ return false unless valid_reference?(program, target, instance)
150
+ when :type_null
151
+ return false unless Internal::TypeChecks.single?(instance, TYPE_NULL)
152
+ when :type_boolean
153
+ return false unless Internal::TypeChecks.single?(instance, TYPE_BOOLEAN)
154
+ when :type_object
155
+ return false unless Internal::TypeChecks.single?(instance, TYPE_OBJECT)
156
+ when :type_array
157
+ return false unless Internal::TypeChecks.single?(instance, TYPE_ARRAY)
158
+ when :type_number
159
+ return false unless Internal::TypeChecks.single?(instance, TYPE_NUMBER)
160
+ when :type_integer
161
+ return false unless Internal::TypeChecks.single?(instance, TYPE_INTEGER)
162
+ when :type_string
163
+ return false unless Internal::TypeChecks.single?(instance, TYPE_STRING)
164
+ when :typed_number
165
+ return false unless instance.is_a?(Numeric) && !instance.is_a?(Complex) && valid_number?(operand, instance)
166
+ when :typed_integer
167
+ return false unless instance.is_a?(Numeric) && !instance.is_a?(Complex) && instance.finite? &&
168
+ instance.to_i == instance && valid_number?(operand, instance)
169
+ when :typed_string
170
+ return false unless instance.is_a?(String) && valid_string?(operand, instance)
171
+ when :typed_array
172
+ return false unless instance.is_a?(Array) && valid_array?(operand, instance)
173
+ when :typed_object
174
+ return false unless instance.is_a?(Hash) && valid_object?(operand, instance)
175
+ when :types
176
+ return false if (operand.mask & instance_type_mask(instance, integer: (operand.mask & TYPE_INTEGER) != 0)).zero?
177
+ when :enum
178
+ return false unless operand.any? { |candidate| json_equal?(candidate, instance) }
179
+ when :const
180
+ return false unless json_equal?(operand, instance)
181
+ when :allOf
182
+ return false unless operand.all? { |child| evaluate_valid(child, instance) }
183
+ when :anyOf
184
+ return false unless operand.any? { |child| evaluate_valid(child, instance) }
185
+ when :oneOf
186
+ matches = 0
187
+ operand.each do |child|
188
+ @budget&.checkpoint
189
+ matches += 1 if evaluate_valid(child, instance)
190
+ return false if matches > 1
191
+ end
192
+ return false unless matches == 1
193
+ when :not
194
+ return false if evaluate_valid(operand, instance)
195
+ when :conditional
196
+ if evaluate_valid(operand.condition, instance)
197
+ return false if operand.then_branch && !evaluate_valid(operand.then_branch, instance)
198
+ elsif operand.else_branch && !evaluate_valid(operand.else_branch, instance)
199
+ return false
200
+ end
201
+ when :number
202
+ return false if instance.is_a?(Numeric) && !instance.is_a?(Complex) && !valid_number?(operand, instance)
203
+ when :string
204
+ return false if instance.is_a?(String) && !valid_string?(operand, instance)
205
+ when :array
206
+ return false if instance.is_a?(Array) && !valid_array?(operand, instance)
207
+ when :object
208
+ return false if instance.is_a?(Hash) && !valid_object?(operand, instance)
209
+ else
210
+ raise "unknown VM instruction #{opcode.inspect}"
211
+ end
212
+ instruction_index += 2
213
+ end
214
+ true
215
+ rescue ResolutionError
216
+ false
217
+ ensure
218
+ leave_scope if entered_scope
219
+ end
220
+
221
+ private def reset_evaluation_pool
222
+ @evaluation_pool.reset
223
+ end
224
+
225
+ private def tracked_evaluation
226
+ @evaluation_pool.acquire
227
+ end
228
+
229
+ private def evaluate(program, instance)
230
+ before = @error_count
231
+ evaluation = Evaluation.valid
232
+ code = program.code
233
+ flags = code[0]
234
+ entered_scope = false
235
+ if (flags & TRACKS_DYNAMIC_SCOPE) != 0
236
+ resource = program.node.resource
237
+ unless @dynamic_scope&.last.equal?(resource)
238
+ (@dynamic_scope ||= []) << resource
239
+ entered_scope = true
240
+ end
241
+ end
242
+
243
+ instruction_index = 1
244
+ while (opcode = code[instruction_index])
245
+ @budget&.checkpoint
246
+ operand = code[instruction_index + 1]
247
+ case opcode
248
+ when :boolean
249
+ if operand == false
250
+ add_error("falseSchema", append_keyword: false) { Internal::ErrorMessage.false_schema }
251
+ evaluation = Evaluation.invalid
252
+ end
253
+ when :ref
254
+ evaluation = evaluation.merge(evaluate_ref(program, operand, instance))
255
+ when :recursive_ref
256
+ evaluation = evaluation.merge(evaluate_recursive_ref(program, operand, instance))
257
+ when :dynamic_ref
258
+ evaluation = evaluation.merge(evaluate_dynamic_ref(program, operand, instance))
259
+ when :type_null
260
+ check_compiled_type("null", Internal::TypeChecks.single?(instance, TYPE_NULL), instance)
261
+ when :type_boolean
262
+ check_compiled_type("boolean", Internal::TypeChecks.single?(instance, TYPE_BOOLEAN), instance)
263
+ when :type_object
264
+ check_compiled_type("object", Internal::TypeChecks.single?(instance, TYPE_OBJECT), instance)
265
+ when :type_array
266
+ check_compiled_type("array", Internal::TypeChecks.single?(instance, TYPE_ARRAY), instance)
267
+ when :type_number
268
+ check_compiled_type("number", Internal::TypeChecks.single?(instance, TYPE_NUMBER), instance)
269
+ when :type_integer
270
+ check_compiled_type("integer", Internal::TypeChecks.single?(instance, TYPE_INTEGER), instance)
271
+ when :type_string
272
+ check_compiled_type("string", Internal::TypeChecks.single?(instance, TYPE_STRING), instance)
273
+ when :typed_number
274
+ if instance.is_a?(Numeric) && !instance.is_a?(Complex)
275
+ check_number(operand, instance)
276
+ else
277
+ check_compiled_type("number", false, instance)
278
+ end
279
+ when :typed_integer
280
+ if instance.is_a?(Numeric) && !instance.is_a?(Complex)
281
+ integer = instance.finite? && instance.to_i == instance
282
+ check_compiled_type("integer", integer, instance)
283
+ check_number(operand, instance)
284
+ else
285
+ check_compiled_type("integer", false, instance)
286
+ end
287
+ when :typed_string
288
+ if instance.is_a?(String)
289
+ check_string(operand, instance)
290
+ else
291
+ check_compiled_type("string", false, instance)
292
+ end
293
+ when :typed_array
294
+ if instance.is_a?(Array)
295
+ result = check_array(operand, instance, evaluation)
296
+ return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
297
+ evaluation = evaluation.merge(result)
298
+ else
299
+ check_compiled_type("array", false, instance)
300
+ end
301
+ when :typed_object
302
+ if instance.is_a?(Hash)
303
+ result = check_object(operand, instance, evaluation)
304
+ return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
305
+ evaluation = evaluation.merge(result)
306
+ else
307
+ check_compiled_type("object", false, instance)
308
+ end
309
+ when :types
310
+ check_compiled_types(operand, instance)
311
+ when :enum
312
+ add_error("enum") { Internal::ErrorMessage.enum } unless operand.any? { |candidate| json_equal?(candidate, instance) }
313
+ when :const
314
+ add_error("const") { Internal::ErrorMessage.const } unless json_equal?(operand, instance)
315
+ when :allOf, :anyOf, :oneOf, :not, :conditional
316
+ result = check_combiner(opcode, operand, instance)
317
+ return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
318
+ evaluation = evaluation.merge(result)
319
+ when :number
320
+ check_number(operand, instance) if instance.is_a?(Numeric) && !instance.is_a?(Complex)
321
+ when :string
322
+ check_string(operand, instance) if instance.is_a?(String)
323
+ when :array
324
+ if instance.is_a?(Array)
325
+ result = check_array(operand, instance, evaluation)
326
+ return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
327
+ evaluation = evaluation.merge(result)
328
+ end
329
+ when :object
330
+ if instance.is_a?(Hash)
331
+ result = check_object(operand, instance, evaluation)
332
+ return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
333
+ evaluation = evaluation.merge(result)
334
+ end
335
+ else
336
+ raise "unknown VM instruction #{opcode.inspect}"
337
+ end
338
+ instruction_index += 2
339
+ end
340
+ (@error_count == before) ? evaluation : Evaluation.invalid
341
+ ensure
342
+ leave_scope if entered_scope
343
+ end
344
+
345
+ private def leave_scope
346
+ @dynamic_scope.pop
347
+ end
348
+
349
+ private def recursive_target(program, rules)
350
+ target = reference_target(program, rules)
351
+ return target unless rules.fragment == "" && target.recursive_anchor?
352
+
353
+ @dynamic_scope&.each do |resource|
354
+ @budget&.checkpoint
355
+ compiled = @compiler.compile(resource.root)
356
+ return compiled if compiled.recursive_anchor?
357
+ end
358
+ target
359
+ end
360
+
361
+ private def dynamic_target(program, rules)
362
+ target = reference_target(program, rules)
363
+ fragment = rules.fragment
364
+ return target if fragment.nil? || fragment.empty? || fragment.start_with?("/")
365
+
366
+ return target unless target.dynamic_anchor == fragment
367
+
368
+ Array(@dynamic_scope).each do |resource|
369
+ @budget&.checkpoint
370
+ dynamic = @graph.dynamic_anchor(resource, fragment)
371
+ return @compiler.compile(dynamic) if dynamic
372
+ end
373
+ target
374
+ end
375
+
376
+ private def valid_reference?(source, target, instance)
377
+ instances = active_instances(source)
378
+ return true if instances.key?(instance) || @native_references&.reference_active?(source, instance)
379
+
380
+ instances[instance] = true
381
+ activated = true
382
+ evaluate_valid(target, instance)
383
+ ensure
384
+ instances&.delete(instance) if activated
385
+ end
386
+
387
+ private def reference_target(program, rules)
388
+ references = (@resolved_references ||= {}.compare_by_identity)
389
+ references.fetch(rules) { references[rules] = @compiler.resolve(program, rules.value) }
390
+ end
391
+
392
+ private def evaluate_ref(source, rules, instance)
393
+ target = reference_target(source, rules)
394
+ evaluate_reference(source, target, instance, "$ref")
395
+ rescue ResolutionError => error
396
+ unresolved_reference("$ref", error)
397
+ end
398
+
399
+ private def evaluate_recursive_ref(source, rules, instance)
400
+ evaluate_reference(source, recursive_target(source, rules), instance, "$recursiveRef")
401
+ rescue ResolutionError => error
402
+ unresolved_reference("$recursiveRef", error)
403
+ end
404
+
405
+ private def evaluate_dynamic_ref(source, rules, instance)
406
+ evaluate_reference(source, dynamic_target(source, rules), instance, "$dynamicRef")
407
+ rescue ResolutionError => error
408
+ unresolved_reference("$dynamicRef", error)
409
+ end
410
+
411
+ private def evaluate_reference(source, target, instance, keyword)
412
+ instances = active_instances(source)
413
+ return Evaluation.valid if instances.key?(instance) || @native_references&.reference_active?(source, instance)
414
+
415
+ instances[instance] = true
416
+ activated = true
417
+ evaluate_at(target, instance, MISSING_SEGMENT, keyword)
418
+ ensure
419
+ instances&.delete(instance) if activated
420
+ end
421
+
422
+ private def unresolved_reference(keyword, error)
423
+ add_error(keyword, error.message, append_keyword: false)
424
+ Evaluation.invalid
425
+ end
426
+
427
+ private def active_instances(program)
428
+ active = (@active ||= {}.compare_by_identity)
429
+ active[program] ||= {}.compare_by_identity
430
+ end
431
+
432
+ private def check_compiled_type(name, valid, value)
433
+ add_error("type") { Internal::ErrorMessage.type(name, value) } unless valid
434
+ end
435
+
436
+ private def check_compiled_types(rules, value)
437
+ instance_mask = instance_type_mask(value, integer: (rules.mask & TYPE_INTEGER) != 0)
438
+ return unless (rules.mask & instance_mask).zero?
439
+
440
+ add_error("type") { Internal::ErrorMessage.type(rules.names, value) }
441
+ end
442
+
443
+ private def instance_type_mask(value, integer:)
444
+ Internal::TypeChecks.instance_mask(value, integer: integer)
445
+ end
446
+
447
+ private def integer?(value)
448
+ Internal::TypeChecks.integer?(value)
449
+ end
450
+
451
+ private def check_number(rules, value)
452
+ mask = rules.mask
453
+ actual = (mask & MULTIPLE_OF).zero? ? value : number_state.decimal(value)
454
+ if (mask & MAXIMUM) != 0 && actual > rules.maximum
455
+ add_error("maximum") { Internal::ErrorMessage.numeric_limit("maximum", rules.maximum) }
456
+ end
457
+ if (mask & MINIMUM) != 0 && actual < rules.minimum
458
+ add_error("minimum") { Internal::ErrorMessage.numeric_limit("minimum", rules.minimum) }
459
+ end
460
+ if (mask & EXCLUSIVE_MAXIMUM) != 0 && actual >= rules.exclusive_maximum
461
+ add_error("exclusiveMaximum") do
462
+ Internal::ErrorMessage.numeric_limit("exclusiveMaximum", rules.exclusive_maximum)
463
+ end
464
+ end
465
+ if (mask & EXCLUSIVE_MINIMUM) != 0 && actual <= rules.exclusive_minimum
466
+ add_error("exclusiveMinimum") do
467
+ Internal::ErrorMessage.numeric_limit("exclusiveMinimum", rules.exclusive_minimum)
468
+ end
469
+ end
470
+ return if (mask & MULTIPLE_OF).zero?
471
+
472
+ divisor = rules.multiple_of
473
+ valid = valid_multiple?(rules, value)
474
+ add_error("multipleOf") { Internal::ErrorMessage.multiple_of(divisor) } unless valid
475
+ end
476
+
477
+ private def check_string(rules, value)
478
+ length = value.length
479
+ if rules.max_length && length > rules.max_length
480
+ add_error("maxLength") { Internal::ErrorMessage.size("maxLength", rules.max_length, length) }
481
+ end
482
+ if rules.min_length && length < rules.min_length
483
+ add_error("minLength") { Internal::ErrorMessage.size("minLength", rules.min_length, length) }
484
+ end
485
+ if rules.pattern && !ecma_regexp(rules.pattern).match?(value)
486
+ add_error("pattern") { Internal::ErrorMessage.pattern(rules.pattern) }
487
+ end
488
+ if rules.format && (@validate_format || rules.format_assertion) && !rules.format.call(value)
489
+ add_error("format") { Internal::ErrorMessage.format(rules.format.name) }
490
+ end
491
+ check_content(rules, value) if @validate_content && (rules.decode_base64 || rules.parse_json)
492
+ rescue RegexpError
493
+ add_error("pattern") { Internal::ErrorMessage.invalid_pattern(rules.pattern) }
494
+ end
495
+
496
+ private def check_content(rules, value)
497
+ decoded = rules.decode_base64 ? Base64.strict_decode64(value) : value
498
+ return unless rules.parse_json
499
+
500
+ JSON.parse(decoded)
501
+ rescue ArgumentError, JSON::ParserError
502
+ keyword = rules.decode_base64 ? "contentEncoding" : "contentMediaType"
503
+ add_error(keyword) do
504
+ (keyword == "contentEncoding") ? Internal::ErrorMessage.content_encoding : Internal::ErrorMessage.content_media_type
505
+ end
506
+ end
507
+
508
+ private def valid_array?(rules, value)
509
+ length = value.length
510
+ return false unless valid_container_bounds?(length, rules.max_items, rules.min_items)
511
+ return false if rules.unique && !unique_items?(value)
512
+
513
+ valid_array_children?(rules, value, length) { |child, item| evaluate_valid(child, item) }
514
+ end
515
+
516
+ private def valid_object?(rules, value)
517
+ length = value.length
518
+ return false unless valid_container_bounds?(length, rules.max_properties, rules.min_properties)
519
+ return false unless valid_required_keys?(rules.required, value)
520
+
521
+ valid_object_children?(rules, value) { |child, item| evaluate_valid(child, item) }
522
+ end
523
+
524
+ private def check_combiner(opcode, operand, value)
525
+ evaluation = Evaluation.valid
526
+ case opcode
527
+ when :allOf
528
+ index = 0
529
+ while index < operand.length
530
+ @budget&.checkpoint
531
+ evaluation = evaluation.merge(evaluate_at(operand[index], value, MISSING_SEGMENT, "allOf", index))
532
+ index += 1
533
+ end
534
+ when :anyOf
535
+ matches = 0
536
+ index = 0
537
+ while index < operand.length
538
+ @budget&.checkpoint
539
+ result = trial(operand[index], value)
540
+ if result.valid?
541
+ matches += 1
542
+ evaluation = evaluation.merge(result)
543
+ end
544
+ index += 1
545
+ end
546
+ add_error("anyOf") { Internal::ErrorMessage.any_of } if matches.zero?
547
+ when :oneOf
548
+ matches = 0
549
+ matched_evaluation = nil
550
+ index = 0
551
+ while index < operand.length
552
+ @budget&.checkpoint
553
+ result = trial(operand[index], value)
554
+ if result.valid?
555
+ matches += 1
556
+ matched_evaluation = result
557
+ end
558
+ index += 1
559
+ end
560
+ if matches == 1
561
+ evaluation = evaluation.merge(matched_evaluation)
562
+ else
563
+ add_error("oneOf") { Internal::ErrorMessage.one_of(matches) }
564
+ end
565
+ when :not
566
+ add_error("not") { Internal::ErrorMessage.not } if trial(operand, value).valid?
567
+ when :conditional
568
+ condition = trial(operand.condition, value)
569
+ condition_valid = condition.valid?
570
+ evaluation = evaluation.merge(condition) if condition_valid
571
+ if condition_valid && operand.then_branch
572
+ evaluation = evaluation.merge(evaluate_at(operand.then_branch, value, MISSING_SEGMENT, "then"))
573
+ elsif !condition_valid && operand.else_branch
574
+ evaluation = evaluation.merge(evaluate_at(operand.else_branch, value, MISSING_SEGMENT, "else"))
575
+ end
576
+ end
577
+ evaluation
578
+ end
579
+
580
+ private def trial(program, value)
581
+ saved_errors = @errors
582
+ saved_count = @error_count
583
+ saved_instance_path = @instance_path
584
+ @errors = nil
585
+ @error_count = 0
586
+ # Trials retain annotations, but discard errors. Descendants therefore
587
+ # need neither instance nor schema paths until the trial has returned.
588
+ if saved_instance_path
589
+ saved_schema_path = @schema_path
590
+ @instance_path = nil
591
+ @schema_path = nil
592
+ end
593
+ evaluate(program, value)
594
+ ensure
595
+ @errors = saved_errors
596
+ @error_count = saved_count
597
+ @native_reports[0] = self if @native_reports
598
+ if saved_instance_path
599
+ @instance_path = saved_instance_path
600
+ @schema_path = saved_schema_path
601
+ end
602
+ end
603
+
604
+ private def evaluate_at(program, instance, instance_segment, schema_segment, child_segment = MISSING_SEGMENT)
605
+ return evaluate(program, instance) unless @instance_path
606
+
607
+ evaluate_at_with_path(program, instance, instance_segment, schema_segment, child_segment)
608
+ end
609
+
610
+ private def evaluate_child_at(program, instance, instance_segment, schema_segment, child_segment = MISSING_SEGMENT)
611
+ return evaluate_at(program, instance, instance_segment, schema_segment, child_segment).valid? if @errors
612
+
613
+ valid = evaluate_valid(program, instance)
614
+ @error_count += 1 unless valid
615
+ valid
616
+ end
617
+
618
+ private def evaluate_at_with_path(program, instance, instance_segment, schema_segment, child_segment)
619
+ @instance_path << instance_segment unless instance_segment.equal?(MISSING_SEGMENT)
620
+ @schema_path << schema_segment
621
+ @schema_path << child_segment unless child_segment.equal?(MISSING_SEGMENT)
622
+ evaluate(program, instance)
623
+ ensure
624
+ @schema_path.pop unless child_segment.equal?(MISSING_SEGMENT)
625
+ @schema_path.pop
626
+ @instance_path.pop unless instance_segment.equal?(MISSING_SEGMENT)
627
+ end
628
+
629
+ private def add_error(keyword, message = nil, append_keyword: true)
630
+ @error_count += 1
631
+ return false unless @errors
632
+
633
+ final_segment = append_keyword ? keyword : MISSING_SEGMENT
634
+ @errors << ValidationError.new(
635
+ keyword: keyword,
636
+ instance_path: pointer(@instance_path),
637
+ schema_path: pointer(@schema_path, final_segment),
638
+ message: message || yield
639
+ )
640
+ false
641
+ end
642
+
643
+ private def number?(value)
644
+ Internal::TypeChecks.number?(value)
645
+ end
646
+
647
+ private def pointer(path, final_segment = MISSING_SEGMENT)
648
+ result = +""
649
+ path.each { |segment| append_pointer_segment(result, segment) }
650
+ append_pointer_segment(result, final_segment) unless final_segment.equal?(MISSING_SEGMENT)
651
+ result
652
+ end
653
+
654
+ private def append_pointer_segment(pointer, segment)
655
+ pointer << "/" << Internal::JsonPointer.escape(segment)
656
+ end
657
+
658
+ private_constant :MISSING_SEGMENT
659
+ end
660
+ end
661
+
662
+ private_constant :VM
663
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../limit_exceeded"
4
+ require_relative "../evaluation_limits"
5
+ require_relative "evaluator"
6
+
7
+ module Schemurai
8
+ module VM
9
+ # VM execution with invocation-local limits; no Ruby evaluator delegation.
10
+ class LimitedEvaluator < Evaluator
11
+ include Internal::ValueEquality::Checkpoints
12
+ include Internal::StringChecks::TimedRegexps
13
+ include Internal::EvaluationLimits
14
+
15
+ private def add_error(keyword, message = nil, append_keyword: true, &block)
16
+ @budget.reserve_error if @errors
17
+ super
18
+ end
19
+
20
+ private def number_state
21
+ state = super
22
+ state.budget = @budget
23
+ state
24
+ end
25
+ end
26
+ end
27
+ end