schemurai 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +67 -4
- data/ext/schemurai_native/Cargo.lock +1 -1
- data/ext/schemurai_native/Cargo.toml +1 -1
- data/ext/schemurai_native/glue.c +155 -14
- data/ext/schemurai_native/src/budget.rs +101 -0
- data/ext/schemurai_native/src/diagnostics.rs +395 -225
- data/ext/schemurai_native/src/enumeration.rs +154 -0
- data/ext/schemurai_native/src/evaluation.rs +219 -0
- data/ext/schemurai_native/src/graph.rs +50 -0
- data/ext/schemurai_native/src/instance_graph.rs +84 -0
- data/ext/schemurai_native/src/lib.rs +947 -184
- data/ext/schemurai_native/src/memo.rs +1149 -0
- data/ext/schemurai_native/src/memo_scope.rs +240 -0
- data/ext/schemurai_native/src/number.rs +7 -8
- data/ext/schemurai_native/src/pattern.rs +157 -14
- data/ext/schemurai_native/src/scope.rs +243 -33
- data/ext/schemurai_native/src/structural.rs +86 -0
- data/ext/schemurai_native/src/tracking.rs +245 -90
- data/lib/schemurai/backend.rb +1 -0
- data/lib/schemurai/evaluator.rb +45 -3
- data/lib/schemurai/limit_budget.rb +78 -0
- data/lib/schemurai/limit_exceeded.rb +18 -0
- data/lib/schemurai/limited_evaluator.rb +100 -0
- data/lib/schemurai/native/evaluator.rb +32 -5
- data/lib/schemurai/schema_domain.rb +12 -7
- data/lib/schemurai/schema_graph.rb +10 -1
- data/lib/schemurai/unique_items.rb +2 -0
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/compiler.rb +7 -3
- data/lib/schemurai/vm/evaluator.rb +50 -3
- data/lib/schemurai/vm/limited_evaluator.rb +97 -0
- data/lib/schemurai.rb +74 -27
- data/sig/schemurai.rbs +20 -12
- metadata +16 -4
data/lib/schemurai/evaluator.rb
CHANGED
|
@@ -148,11 +148,13 @@ module Schemurai
|
|
|
148
148
|
schema = node.schema
|
|
149
149
|
if schema.key?("allOf")
|
|
150
150
|
schema["allOf"].each_index do |index|
|
|
151
|
+
@budget&.checkpoint
|
|
151
152
|
return false unless evaluate_valid(node.child("allOf", index), value)
|
|
152
153
|
end
|
|
153
154
|
end
|
|
154
155
|
if schema.key?("anyOf")
|
|
155
156
|
matched = schema["anyOf"].each_index.any? do |index|
|
|
157
|
+
@budget&.checkpoint
|
|
156
158
|
evaluate_valid(node.child("anyOf", index), value)
|
|
157
159
|
end
|
|
158
160
|
return false unless matched
|
|
@@ -160,6 +162,7 @@ module Schemurai
|
|
|
160
162
|
if schema.key?("oneOf")
|
|
161
163
|
matches = 0
|
|
162
164
|
schema["oneOf"].each_index do |index|
|
|
165
|
+
@budget&.checkpoint
|
|
163
166
|
matches += 1 if evaluate_valid(node.child("oneOf", index), value)
|
|
164
167
|
return false if matches > 1
|
|
165
168
|
end
|
|
@@ -234,6 +237,7 @@ module Schemurai
|
|
|
234
237
|
prefix_items = schema["prefixItems"]
|
|
235
238
|
if prefix_items.is_a?(Array)
|
|
236
239
|
prefix_items.each_index do |index|
|
|
240
|
+
@budget&.checkpoint
|
|
237
241
|
break if index >= length
|
|
238
242
|
return false unless evaluate_valid(node.child("prefixItems", index), value[index])
|
|
239
243
|
end
|
|
@@ -242,12 +246,14 @@ module Schemurai
|
|
|
242
246
|
items = schema["items"]
|
|
243
247
|
if items.is_a?(Array)
|
|
244
248
|
items.each_index do |index|
|
|
249
|
+
@budget&.checkpoint
|
|
245
250
|
break if index >= length
|
|
246
251
|
return false unless evaluate_valid(node.child("items", index), value[index])
|
|
247
252
|
end
|
|
248
253
|
if length > items.length && schema.key?("additionalItems")
|
|
249
254
|
additional = node.child("additionalItems")
|
|
250
255
|
(items.length...length).each do |index|
|
|
256
|
+
@budget&.checkpoint
|
|
251
257
|
return false unless evaluate_valid(additional, value[index])
|
|
252
258
|
end
|
|
253
259
|
end
|
|
@@ -255,6 +261,7 @@ module Schemurai
|
|
|
255
261
|
child = node.child("items")
|
|
256
262
|
start = prefix_items.is_a?(Array) ? prefix_items.length : 0
|
|
257
263
|
(start...length).each do |index|
|
|
264
|
+
@budget&.checkpoint
|
|
258
265
|
return false unless evaluate_valid(child, value[index])
|
|
259
266
|
end
|
|
260
267
|
end
|
|
@@ -277,12 +284,16 @@ module Schemurai
|
|
|
277
284
|
length = value.length
|
|
278
285
|
return false if schema.key?("maxProperties") && length > schema["maxProperties"]
|
|
279
286
|
return false if schema.key?("minProperties") && length < schema["minProperties"]
|
|
280
|
-
return false if schema.key?("required") && !schema["required"].all?
|
|
287
|
+
return false if schema.key?("required") && !schema["required"].all? do |name|
|
|
288
|
+
@budget&.checkpoint
|
|
289
|
+
value.key?(name)
|
|
290
|
+
end
|
|
281
291
|
|
|
282
292
|
properties = schema["properties"]
|
|
283
293
|
patterns = schema["patternProperties"]
|
|
284
294
|
additional = node.child("additionalProperties") if schema.key?("additionalProperties")
|
|
285
295
|
value.each do |name, property_value|
|
|
296
|
+
@budget&.checkpoint
|
|
286
297
|
matched = false
|
|
287
298
|
if properties&.key?(name)
|
|
288
299
|
matched = true
|
|
@@ -290,6 +301,7 @@ module Schemurai
|
|
|
290
301
|
end
|
|
291
302
|
if patterns
|
|
292
303
|
patterns.each_key do |pattern|
|
|
304
|
+
@budget&.checkpoint
|
|
293
305
|
next unless ecma_regexp(pattern).match?(name)
|
|
294
306
|
matched = true
|
|
295
307
|
return false unless evaluate_valid(node.child("patternProperties", pattern), property_value)
|
|
@@ -305,9 +317,13 @@ module Schemurai
|
|
|
305
317
|
|
|
306
318
|
if schema.key?("dependencies")
|
|
307
319
|
schema["dependencies"].each do |name, dependency|
|
|
320
|
+
@budget&.checkpoint
|
|
308
321
|
next unless value.key?(name)
|
|
309
322
|
if dependency.is_a?(Array)
|
|
310
|
-
return false unless dependency.all?
|
|
323
|
+
return false unless dependency.all? do |required_name|
|
|
324
|
+
@budget&.checkpoint
|
|
325
|
+
value.key?(required_name)
|
|
326
|
+
end
|
|
311
327
|
else
|
|
312
328
|
return false unless evaluate_valid(node.child("dependencies", name), value)
|
|
313
329
|
end
|
|
@@ -315,13 +331,18 @@ module Schemurai
|
|
|
315
331
|
end
|
|
316
332
|
if schema.key?("dependentRequired")
|
|
317
333
|
schema["dependentRequired"].each do |name, required_names|
|
|
334
|
+
@budget&.checkpoint
|
|
318
335
|
next unless value.key?(name)
|
|
319
|
-
return false unless required_names.all?
|
|
336
|
+
return false unless required_names.all? do |required_name|
|
|
337
|
+
@budget&.checkpoint
|
|
338
|
+
value.key?(required_name)
|
|
339
|
+
end
|
|
320
340
|
end
|
|
321
341
|
end
|
|
322
342
|
|
|
323
343
|
if schema.key?("dependentSchemas")
|
|
324
344
|
schema["dependentSchemas"].each_key do |name|
|
|
345
|
+
@budget&.checkpoint
|
|
325
346
|
next unless value.key?(name)
|
|
326
347
|
return false unless evaluate_valid(node.child("dependentSchemas", name), value)
|
|
327
348
|
end
|
|
@@ -425,6 +446,7 @@ module Schemurai
|
|
|
425
446
|
return target unless target.schema.is_a?(Hash) && target.schema["$dynamicAnchor"] == raw_fragment
|
|
426
447
|
|
|
427
448
|
Array(@dynamic_scope).each do |resource|
|
|
449
|
+
@budget&.checkpoint
|
|
428
450
|
dynamic = @graph.dynamic_anchor(resource, raw_fragment)
|
|
429
451
|
return dynamic if dynamic
|
|
430
452
|
end
|
|
@@ -459,6 +481,7 @@ module Schemurai
|
|
|
459
481
|
evaluation = Evaluation.valid
|
|
460
482
|
if schema.key?("allOf")
|
|
461
483
|
schema["allOf"].each_index do |index|
|
|
484
|
+
@budget&.checkpoint
|
|
462
485
|
evaluation = evaluation.merge(
|
|
463
486
|
evaluate_at(node.child("allOf", index), value, MISSING_SEGMENT, "allOf", index)
|
|
464
487
|
)
|
|
@@ -468,6 +491,7 @@ module Schemurai
|
|
|
468
491
|
if schema.key?("anyOf")
|
|
469
492
|
matches = []
|
|
470
493
|
schema["anyOf"].each_index do |index|
|
|
494
|
+
@budget&.checkpoint
|
|
471
495
|
result = trial_at(node.child("anyOf", index), value, MISSING_SEGMENT, "anyOf", index)
|
|
472
496
|
matches << result if result.valid?
|
|
473
497
|
end
|
|
@@ -481,6 +505,7 @@ module Schemurai
|
|
|
481
505
|
if schema.key?("oneOf")
|
|
482
506
|
matches = []
|
|
483
507
|
schema["oneOf"].each_index do |index|
|
|
508
|
+
@budget&.checkpoint
|
|
484
509
|
result = trial_at(node.child("oneOf", index), value, MISSING_SEGMENT, "oneOf", index)
|
|
485
510
|
matches << result if result.valid?
|
|
486
511
|
end
|
|
@@ -559,6 +584,7 @@ module Schemurai
|
|
|
559
584
|
prefix_items = schema["prefixItems"]
|
|
560
585
|
if prefix_items.is_a?(Array)
|
|
561
586
|
prefix_items.each_index do |index|
|
|
587
|
+
@budget&.checkpoint
|
|
562
588
|
break if index >= value.length
|
|
563
589
|
evaluate_at(node.child("prefixItems", index), value[index], index, "prefixItems", index)
|
|
564
590
|
evaluated << index
|
|
@@ -568,6 +594,7 @@ module Schemurai
|
|
|
568
594
|
items = schema["items"]
|
|
569
595
|
if items.is_a?(Array)
|
|
570
596
|
items.each_index do |index|
|
|
597
|
+
@budget&.checkpoint
|
|
571
598
|
break if index >= value.length
|
|
572
599
|
evaluate_at(node.child("items", index), value[index], index, "items", index)
|
|
573
600
|
evaluated << index
|
|
@@ -575,6 +602,7 @@ module Schemurai
|
|
|
575
602
|
if value.length > items.length && schema.key?("additionalItems")
|
|
576
603
|
additional = node.child("additionalItems")
|
|
577
604
|
(items.length...value.length).each do |index|
|
|
605
|
+
@budget&.checkpoint
|
|
578
606
|
evaluate_at(additional, value[index], index, "additionalItems")
|
|
579
607
|
evaluated << index
|
|
580
608
|
end
|
|
@@ -582,6 +610,7 @@ module Schemurai
|
|
|
582
610
|
elsif !items.nil?
|
|
583
611
|
start = prefix_items.is_a?(Array) ? prefix_items.length : 0
|
|
584
612
|
value.each_with_index do |item, index|
|
|
613
|
+
@budget&.checkpoint
|
|
585
614
|
next if index < start
|
|
586
615
|
evaluate_at(node.child("items"), item, index, "items")
|
|
587
616
|
evaluated << index
|
|
@@ -590,6 +619,7 @@ module Schemurai
|
|
|
590
619
|
|
|
591
620
|
if schema.key?("contains")
|
|
592
621
|
matched = value.each_index.select do |index|
|
|
622
|
+
@budget&.checkpoint
|
|
593
623
|
trial_at(node.child("contains"), value[index], index, "contains").valid?
|
|
594
624
|
end
|
|
595
625
|
minimum = schema.fetch("minContains", 1)
|
|
@@ -604,6 +634,7 @@ module Schemurai
|
|
|
604
634
|
if schema.key?("unevaluatedItems")
|
|
605
635
|
unevaluated = (0...value.length).to_a - combined
|
|
606
636
|
unevaluated.each do |index|
|
|
637
|
+
@budget&.checkpoint
|
|
607
638
|
evaluate_at(node.child("unevaluatedItems"), value[index], index, "unevaluatedItems")
|
|
608
639
|
end
|
|
609
640
|
evaluated.concat(unevaluated)
|
|
@@ -618,12 +649,14 @@ module Schemurai
|
|
|
618
649
|
limit(schema, "minProperties", value.length) { |actual, expected| actual >= expected }
|
|
619
650
|
|
|
620
651
|
Array(schema["required"]).each do |name|
|
|
652
|
+
@budget&.checkpoint
|
|
621
653
|
add_error("required") { ErrorMessage.required(name) } unless value.key?(name)
|
|
622
654
|
end
|
|
623
655
|
|
|
624
656
|
properties = schema.fetch("properties", {})
|
|
625
657
|
patterns = schema.fetch("patternProperties", {})
|
|
626
658
|
value.each do |name, property_value|
|
|
659
|
+
@budget&.checkpoint
|
|
627
660
|
matched = false
|
|
628
661
|
if properties.key?(name)
|
|
629
662
|
matched = true
|
|
@@ -631,6 +664,7 @@ module Schemurai
|
|
|
631
664
|
evaluated << name
|
|
632
665
|
end
|
|
633
666
|
patterns.each do |pattern, subschema|
|
|
667
|
+
@budget&.checkpoint
|
|
634
668
|
next unless ecma_regexp(pattern).match?(name)
|
|
635
669
|
matched = true
|
|
636
670
|
evaluate_at(node.child("patternProperties", pattern), property_value, name, "patternProperties", pattern)
|
|
@@ -644,14 +678,17 @@ module Schemurai
|
|
|
644
678
|
|
|
645
679
|
if schema.key?("propertyNames")
|
|
646
680
|
value.each_key do |name|
|
|
681
|
+
@budget&.checkpoint
|
|
647
682
|
evaluate_at(node.child("propertyNames"), name, name, "propertyNames")
|
|
648
683
|
end
|
|
649
684
|
end
|
|
650
685
|
|
|
651
686
|
schema.fetch("dependencies", {}).each do |name, dependency|
|
|
687
|
+
@budget&.checkpoint
|
|
652
688
|
next unless value.key?(name)
|
|
653
689
|
if dependency.is_a?(Array)
|
|
654
690
|
dependency.each do |required_name|
|
|
691
|
+
@budget&.checkpoint
|
|
655
692
|
add_error("dependencies") { ErrorMessage.dependent_required(name, required_name) } unless value.key?(required_name)
|
|
656
693
|
end
|
|
657
694
|
else
|
|
@@ -661,8 +698,10 @@ module Schemurai
|
|
|
661
698
|
end
|
|
662
699
|
|
|
663
700
|
schema.fetch("dependentRequired", {}).each do |name, required_names|
|
|
701
|
+
@budget&.checkpoint
|
|
664
702
|
next unless value.key?(name)
|
|
665
703
|
required_names.each do |required_name|
|
|
704
|
+
@budget&.checkpoint
|
|
666
705
|
unless value.key?(required_name)
|
|
667
706
|
add_error("dependentRequired") { ErrorMessage.dependent_required(name, required_name) }
|
|
668
707
|
end
|
|
@@ -670,6 +709,7 @@ module Schemurai
|
|
|
670
709
|
end
|
|
671
710
|
|
|
672
711
|
schema.fetch("dependentSchemas", {}).each_key do |name|
|
|
712
|
+
@budget&.checkpoint
|
|
673
713
|
next unless value.key?(name)
|
|
674
714
|
result = evaluate_at(node.child("dependentSchemas", name), value, MISSING_SEGMENT, "dependentSchemas", name)
|
|
675
715
|
evaluated.concat(result.evaluated_properties) if result.valid?
|
|
@@ -679,6 +719,7 @@ module Schemurai
|
|
|
679
719
|
if schema.key?("unevaluatedProperties")
|
|
680
720
|
unevaluated = value.keys - combined
|
|
681
721
|
unevaluated.each do |name|
|
|
722
|
+
@budget&.checkpoint
|
|
682
723
|
evaluate_at(node.child("unevaluatedProperties"), value[name], name, "unevaluatedProperties")
|
|
683
724
|
end
|
|
684
725
|
evaluated.concat(unevaluated)
|
|
@@ -757,6 +798,7 @@ module Schemurai
|
|
|
757
798
|
escaped = false
|
|
758
799
|
in_class = false
|
|
759
800
|
pattern.each_char do |character|
|
|
801
|
+
@budget&.checkpoint
|
|
760
802
|
if escaped
|
|
761
803
|
translated << case character
|
|
762
804
|
when "d" then in_class ? "0-9" : "[0-9]"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
# Mutable state owned by one validation call, including native/VM fallback.
|
|
6
|
+
class LimitBudget
|
|
7
|
+
CHECK_INTERVAL = 256
|
|
8
|
+
|
|
9
|
+
def self.validate_options!(max_errors, timeout)
|
|
10
|
+
valid_errors = max_errors.nil? || (max_errors.is_a?(Integer) && max_errors >= 0)
|
|
11
|
+
unless valid_errors
|
|
12
|
+
raise ArgumentError, "max_errors must be a nonnegative Integer or nil"
|
|
13
|
+
end
|
|
14
|
+
valid_timeout = timeout.nil? || ((timeout.is_a?(Integer) || timeout.is_a?(Float)) && timeout >= 0 && timeout.to_f.finite?)
|
|
15
|
+
unless valid_timeout
|
|
16
|
+
raise ArgumentError, "timeout must be finite nonnegative seconds or nil"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(max_errors: nil, timeout: nil)
|
|
21
|
+
@max_errors = max_errors
|
|
22
|
+
@timeout = timeout
|
|
23
|
+
@errors = 0
|
|
24
|
+
@checks = 0
|
|
25
|
+
if timeout
|
|
26
|
+
@started = monotonic_time
|
|
27
|
+
@deadline = @started + timeout
|
|
28
|
+
check_deadline!
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Synchronous polling only; never inject exceptions into another thread.
|
|
33
|
+
def checkpoint
|
|
34
|
+
return unless @deadline
|
|
35
|
+
|
|
36
|
+
@checks += 1
|
|
37
|
+
check_deadline! if (@checks % CHECK_INTERVAL).zero?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def check_deadline!
|
|
41
|
+
return unless @deadline
|
|
42
|
+
|
|
43
|
+
now = monotonic_time
|
|
44
|
+
raise LimitExceeded.new(:timeout, @timeout, now - @started) if now >= @deadline
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Ruby retains the original deadline through native execution and fallback.
|
|
48
|
+
def native_duration
|
|
49
|
+
check_deadline!
|
|
50
|
+
@deadline && [@deadline - monotonic_time, 0.0].max
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def native_max_errors
|
|
54
|
+
@max_errors && [@max_errors, (1 << (0.size * 8)) - 1].min
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def raise_native_limit(kind, attempted)
|
|
58
|
+
if kind == 2
|
|
59
|
+
raise LimitExceeded.new(:timeout, @timeout, monotonic_time - @started)
|
|
60
|
+
end
|
|
61
|
+
raise LimitExceeded.new(:max_errors, @max_errors, attempted)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check before constructing the next message and JSON Pointers.
|
|
65
|
+
def reserve_error
|
|
66
|
+
checkpoint
|
|
67
|
+
attempted = @errors + 1
|
|
68
|
+
raise LimitExceeded.new(:max_errors, @max_errors, attempted) if @max_errors && attempted > @max_errors
|
|
69
|
+
|
|
70
|
+
@errors = attempted
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private def monotonic_time
|
|
74
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
# Raised when validation cannot finish within an application-supplied limit.
|
|
5
|
+
# This is an incomplete evaluation, not a JSON Schema validation failure.
|
|
6
|
+
class LimitExceeded < Error
|
|
7
|
+
# Limit name, configured maximum and attempted count (or elapsed seconds).
|
|
8
|
+
attr_reader :limit, :maximum, :attempted
|
|
9
|
+
|
|
10
|
+
# Creates an interruption error without embedding the input or partial errors.
|
|
11
|
+
def initialize(limit, maximum, attempted)
|
|
12
|
+
@limit = limit
|
|
13
|
+
@maximum = maximum
|
|
14
|
+
@attempted = attempted
|
|
15
|
+
super("validation exceeded #{limit} (maximum #{maximum}, attempted #{attempted})")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "limit_exceeded"
|
|
4
|
+
require_relative "limit_budget"
|
|
5
|
+
require_relative "evaluator"
|
|
6
|
+
|
|
7
|
+
module Schemurai
|
|
8
|
+
module Internal
|
|
9
|
+
# Ruby evaluation with invocation-local limits.
|
|
10
|
+
class LimitedEvaluator < Evaluator
|
|
11
|
+
def validate(instance, max_errors: nil, timeout: nil, budget: nil)
|
|
12
|
+
@budget = budget || LimitBudget.new(max_errors: max_errors, timeout: timeout)
|
|
13
|
+
errors = []
|
|
14
|
+
each_error(instance) do |error|
|
|
15
|
+
errors << error
|
|
16
|
+
end
|
|
17
|
+
@budget.check_deadline!
|
|
18
|
+
Result.new(errors)
|
|
19
|
+
ensure
|
|
20
|
+
@budget = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def valid?(instance, max_errors: nil, timeout: nil, budget: nil)
|
|
24
|
+
@budget = budget || LimitBudget.new(max_errors: max_errors, timeout: timeout)
|
|
25
|
+
result = super(instance)
|
|
26
|
+
@budget.check_deadline!
|
|
27
|
+
result
|
|
28
|
+
ensure
|
|
29
|
+
@budget = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private def evaluate(node, instance)
|
|
33
|
+
@budget.checkpoint
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private def evaluate_valid(node, instance)
|
|
38
|
+
@budget.checkpoint
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private def add_error(keyword, message = nil, append_keyword: true, &block)
|
|
43
|
+
@budget.reserve_error if @error_callback
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private def valid_string?(node, value)
|
|
48
|
+
@budget.check_deadline!
|
|
49
|
+
result = super
|
|
50
|
+
@budget.check_deadline!
|
|
51
|
+
result
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private def check_string(node, value)
|
|
55
|
+
@budget.check_deadline!
|
|
56
|
+
result = super
|
|
57
|
+
@budget.check_deadline!
|
|
58
|
+
result
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private def decimal(value)
|
|
62
|
+
@budget.check_deadline!
|
|
63
|
+
result = super
|
|
64
|
+
@budget.check_deadline!
|
|
65
|
+
result
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private def ecma_regexp(pattern)
|
|
69
|
+
@budget.check_deadline!
|
|
70
|
+
result = super
|
|
71
|
+
@budget.check_deadline!
|
|
72
|
+
result
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private def valid_content?(schema, value)
|
|
76
|
+
@budget.check_deadline!
|
|
77
|
+
result = super
|
|
78
|
+
@budget.check_deadline!
|
|
79
|
+
result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private def check_content(schema, value)
|
|
83
|
+
@budget.check_deadline!
|
|
84
|
+
result = super
|
|
85
|
+
@budget.check_deadline!
|
|
86
|
+
result
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private def json_equal?(left, right)
|
|
90
|
+
@budget.checkpoint
|
|
91
|
+
super
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private def json_fingerprint(value)
|
|
95
|
+
@budget.checkpoint
|
|
96
|
+
super
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../vm/
|
|
3
|
+
require_relative "../vm/limited_evaluator"
|
|
4
4
|
require "schemurai_native"
|
|
5
5
|
require_relative "diagnostics"
|
|
6
6
|
|
|
@@ -9,22 +9,49 @@ module Schemurai
|
|
|
9
9
|
class Evaluator
|
|
10
10
|
def backend = :native
|
|
11
11
|
|
|
12
|
-
def initialize(graph, compiler, root, content: false, format: false)
|
|
13
|
-
@
|
|
12
|
+
def initialize(graph, compiler, root, content: false, format: false, compiled: nil)
|
|
13
|
+
@graph = graph
|
|
14
|
+
@compiler = compiler
|
|
15
|
+
@root = root
|
|
16
|
+
@content = content
|
|
17
|
+
@format = format
|
|
18
|
+
@compiled = compiled || Program.new(root, compiler, format, content)
|
|
19
|
+
@program = @compiled.runner(root)
|
|
14
20
|
@vm = VM::Evaluator.new(graph, compiler, root, content: content, format: format)
|
|
15
21
|
end
|
|
16
22
|
|
|
17
|
-
def valid?(instance)
|
|
23
|
+
def valid?(instance, max_errors: nil, timeout: nil)
|
|
24
|
+
if !max_errors.nil? || !timeout.nil?
|
|
25
|
+
budget = Internal::LimitBudget.new(max_errors: max_errors, timeout: timeout)
|
|
26
|
+
result = @program.valid_with_budget(instance, budget)
|
|
27
|
+
budget.check_deadline!
|
|
28
|
+
return result.nil? ? limited_vm.valid?(instance, budget: budget) : result
|
|
29
|
+
end
|
|
18
30
|
result = @program.valid?(instance)
|
|
19
31
|
result.nil? ? @vm.valid?(instance) : result
|
|
20
32
|
end
|
|
21
33
|
|
|
22
|
-
def validate(instance)
|
|
34
|
+
def validate(instance, max_errors: nil, timeout: nil)
|
|
35
|
+
if !max_errors.nil? || !timeout.nil?
|
|
36
|
+
budget = Internal::LimitBudget.new(max_errors: max_errors, timeout: timeout)
|
|
37
|
+
rows = @program.diagnostics_with_budget(instance, budget)
|
|
38
|
+
return limited_vm.validate(instance, budget: budget) if rows.nil?
|
|
39
|
+
|
|
40
|
+
errors = rows.map do |row|
|
|
41
|
+
budget.reserve_error
|
|
42
|
+
Diagnostics.error(row)
|
|
43
|
+
end
|
|
44
|
+
budget.check_deadline!
|
|
45
|
+
return Result.new(errors)
|
|
46
|
+
end
|
|
23
47
|
rows = @program.diagnostics(instance)
|
|
24
48
|
return @vm.validate(instance) if rows.nil?
|
|
25
49
|
|
|
26
50
|
Result.new(rows.empty? ? rows : rows.map { |row| Diagnostics.error(row) })
|
|
27
51
|
end
|
|
52
|
+
private def limited_vm
|
|
53
|
+
VM::LimitedEvaluator.new(@graph, @compiler, @root, content: @content, format: @format)
|
|
54
|
+
end
|
|
28
55
|
end
|
|
29
56
|
end
|
|
30
57
|
|
|
@@ -8,7 +8,7 @@ module Schemurai
|
|
|
8
8
|
invalid!(label, "must be a boolean or an object")
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
validate_value!(schema, label, [], {})
|
|
11
|
+
validate_value!(schema, label, [], {}, {})
|
|
12
12
|
schema
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -23,7 +23,7 @@ module Schemurai
|
|
|
23
23
|
schemas
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
module_function def validate_value!(value, label, path, ancestors)
|
|
26
|
+
module_function def validate_value!(value, label, path, ancestors, completed)
|
|
27
27
|
case value
|
|
28
28
|
when nil, true, false
|
|
29
29
|
nil
|
|
@@ -34,21 +34,21 @@ module Schemurai
|
|
|
34
34
|
invalid_at!(label, path, "uses a Float subclass") unless value.instance_of?(Float)
|
|
35
35
|
when Array
|
|
36
36
|
invalid_at!(label, path, "uses an Array subclass") unless value.instance_of?(Array)
|
|
37
|
-
descend!(value, label, path, ancestors) do
|
|
37
|
+
descend!(value, label, path, ancestors, completed) do
|
|
38
38
|
value.each_with_index do |item, index|
|
|
39
39
|
path << index
|
|
40
|
-
validate_value!(item, label, path, ancestors)
|
|
40
|
+
validate_value!(item, label, path, ancestors, completed)
|
|
41
41
|
path.pop
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
when Hash
|
|
45
45
|
invalid_at!(label, path, "uses a Hash subclass") unless value.instance_of?(Hash)
|
|
46
|
-
descend!(value, label, path, ancestors) do
|
|
46
|
+
descend!(value, label, path, ancestors, completed) do
|
|
47
47
|
value.each do |key, item|
|
|
48
48
|
invalid_at!(label, path, "contains a non-string object key") unless key.instance_of?(String)
|
|
49
49
|
|
|
50
50
|
path << key
|
|
51
|
-
validate_value!(item, label, path, ancestors)
|
|
51
|
+
validate_value!(item, label, path, ancestors, completed)
|
|
52
52
|
path.pop
|
|
53
53
|
end
|
|
54
54
|
end
|
|
@@ -63,11 +63,16 @@ module Schemurai
|
|
|
63
63
|
end
|
|
64
64
|
private_class_method :supported_exact_scalar?
|
|
65
65
|
|
|
66
|
-
module_function def descend!(value, label, path, ancestors)
|
|
66
|
+
module_function def descend!(value, label, path, ancestors, completed)
|
|
67
|
+
# A completed subtree cannot contain a cycle back to this path. Only
|
|
68
|
+
# publish after the entire subtree passes, preserving first-error paths.
|
|
69
|
+
return if completed.key?(value.object_id)
|
|
70
|
+
|
|
67
71
|
invalid_at!(label, path, "contains a recursive container") if ancestors.key?(value.object_id)
|
|
68
72
|
|
|
69
73
|
ancestors[value.object_id] = true
|
|
70
74
|
yield
|
|
75
|
+
completed[value.object_id] = true
|
|
71
76
|
ensure
|
|
72
77
|
ancestors.delete(value.object_id)
|
|
73
78
|
end
|
|
@@ -53,8 +53,10 @@ module Schemurai
|
|
|
53
53
|
|
|
54
54
|
class Resource
|
|
55
55
|
attr_reader :uri, :root
|
|
56
|
+
attr_accessor :dynamic_anchor_count
|
|
56
57
|
|
|
57
58
|
def initialize(uri, root)
|
|
59
|
+
@dynamic_anchor_count = 0
|
|
58
60
|
@uri = uri
|
|
59
61
|
@root = root
|
|
60
62
|
@nodes = nil
|
|
@@ -144,6 +146,12 @@ module Schemurai
|
|
|
144
146
|
@dynamic_anchors.dig(resource, name)
|
|
145
147
|
end
|
|
146
148
|
|
|
149
|
+
# Anchor tables grow in insertion order, including late external indexing.
|
|
150
|
+
# Native compilation uses this snapshot to avoid absent pair lookups.
|
|
151
|
+
def dynamic_anchor_names(resource)
|
|
152
|
+
@dynamic_anchors[resource]&.keys || []
|
|
153
|
+
end
|
|
154
|
+
|
|
147
155
|
def dynamic_scope?
|
|
148
156
|
@dynamic_scope
|
|
149
157
|
end
|
|
@@ -195,7 +203,8 @@ module Schemurai
|
|
|
195
203
|
end
|
|
196
204
|
end
|
|
197
205
|
changes.dynamic_anchors.each do |resource, anchors|
|
|
198
|
-
(@dynamic_anchors[resource] ||= {}).update(anchors)
|
|
206
|
+
merged = (@dynamic_anchors[resource] ||= {}).update(anchors)
|
|
207
|
+
resource.dynamic_anchor_count = merged.size
|
|
199
208
|
end
|
|
200
209
|
(@custom_dialects ||= {}).update(changes.custom_dialects) unless changes.custom_dialects.empty?
|
|
201
210
|
@dynamic_scope = true if changes.requires_dynamic_scope
|
|
@@ -7,6 +7,7 @@ module Schemurai
|
|
|
7
7
|
# Avoid building an index for small arrays and early duplicates.
|
|
8
8
|
if values.length <= 16
|
|
9
9
|
values.each_with_index do |item, index|
|
|
10
|
+
@budget&.checkpoint
|
|
10
11
|
index.times { |previous| return false if json_equal?(values[previous], item) }
|
|
11
12
|
end
|
|
12
13
|
return true
|
|
@@ -16,6 +17,7 @@ module Schemurai
|
|
|
16
17
|
# Store a single index until a collision needs a bucket array.
|
|
17
18
|
index = 0
|
|
18
19
|
while index < values.length
|
|
20
|
+
@budget&.checkpoint
|
|
19
21
|
item = values[index]
|
|
20
22
|
fingerprint = json_fingerprint(item)
|
|
21
23
|
if (bucket = buckets[fingerprint])
|
data/lib/schemurai/version.rb
CHANGED
|
@@ -391,12 +391,16 @@ module Schemurai
|
|
|
391
391
|
compiled.freeze unless compiled.empty?
|
|
392
392
|
end
|
|
393
393
|
|
|
394
|
-
|
|
394
|
+
# Keep aliases inside one operand while copying away from caller-owned
|
|
395
|
+
# containers. A fresh identity table also rechecks later compilations.
|
|
396
|
+
private def snapshot(value, copies = {}.compare_by_identity)
|
|
397
|
+
return copies[value] if copies.key?(value)
|
|
398
|
+
|
|
395
399
|
case value
|
|
396
400
|
when Hash
|
|
397
|
-
value.to_h { |key, item| [snapshot(key), snapshot(item)] }.freeze
|
|
401
|
+
copies[value] = value.to_h { |key, item| [snapshot(key, copies), snapshot(item, copies)] }.freeze
|
|
398
402
|
when Array
|
|
399
|
-
value.map { |item| snapshot(item) }.freeze
|
|
403
|
+
copies[value] = value.map { |item| snapshot(item, copies) }.freeze
|
|
400
404
|
when String
|
|
401
405
|
-value
|
|
402
406
|
else
|