schemurai 2.3.0 → 3.0.1

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -4
  3. data/ext/schemurai_native/Cargo.lock +8 -1
  4. data/ext/schemurai_native/Cargo.toml +2 -1
  5. data/ext/schemurai_native/glue.c +155 -14
  6. data/ext/schemurai_native/src/budget.rs +101 -0
  7. data/ext/schemurai_native/src/content.rs +18 -0
  8. data/ext/schemurai_native/src/diagnostics.rs +395 -225
  9. data/ext/schemurai_native/src/enumeration.rs +154 -0
  10. data/ext/schemurai_native/src/evaluation.rs +219 -0
  11. data/ext/schemurai_native/src/format.rs +118 -8
  12. data/ext/schemurai_native/src/graph.rs +50 -0
  13. data/ext/schemurai_native/src/instance_graph.rs +84 -0
  14. data/ext/schemurai_native/src/integer_scan.rs +132 -0
  15. data/ext/schemurai_native/src/lib.rs +954 -185
  16. data/ext/schemurai_native/src/memo.rs +1149 -0
  17. data/ext/schemurai_native/src/memo_scope.rs +240 -0
  18. data/ext/schemurai_native/src/number.rs +7 -8
  19. data/ext/schemurai_native/src/pattern.rs +191 -22
  20. data/ext/schemurai_native/src/scope.rs +243 -33
  21. data/ext/schemurai_native/src/structural.rs +86 -0
  22. data/ext/schemurai_native/src/tracking.rs +245 -90
  23. data/lib/schemurai/backend.rb +1 -0
  24. data/lib/schemurai/evaluator.rb +45 -3
  25. data/lib/schemurai/limit_budget.rb +78 -0
  26. data/lib/schemurai/limit_exceeded.rb +18 -0
  27. data/lib/schemurai/limited_evaluator.rb +100 -0
  28. data/lib/schemurai/native/evaluator.rb +32 -5
  29. data/lib/schemurai/schema_domain.rb +12 -7
  30. data/lib/schemurai/schema_graph.rb +10 -1
  31. data/lib/schemurai/unique_items.rb +2 -0
  32. data/lib/schemurai/version.rb +1 -1
  33. data/lib/schemurai/vm/compiler.rb +7 -3
  34. data/lib/schemurai/vm/evaluator.rb +50 -3
  35. data/lib/schemurai/vm/limited_evaluator.rb +97 -0
  36. data/lib/schemurai.rb +74 -27
  37. data/sig/schemurai.rbs +20 -12
  38. metadata +17 -4
@@ -127,6 +127,7 @@ module Schemurai
127
127
  end
128
128
  instruction_index = 1
129
129
  while (opcode = code[instruction_index])
130
+ @budget&.checkpoint
130
131
  operand = code[instruction_index + 1]
131
132
  case opcode
132
133
  when :boolean
@@ -178,6 +179,7 @@ module Schemurai
178
179
  when :oneOf
179
180
  matches = 0
180
181
  operand.each do |child|
182
+ @budget&.checkpoint
181
183
  matches += 1 if evaluate_valid(child, instance)
182
184
  return false if matches > 1
183
185
  end
@@ -237,6 +239,7 @@ module Schemurai
237
239
 
238
240
  instruction_index = 1
239
241
  while (opcode = code[instruction_index])
242
+ @budget&.checkpoint
240
243
  operand = code[instruction_index + 1]
241
244
  case opcode
242
245
  when :boolean
@@ -345,6 +348,7 @@ module Schemurai
345
348
  return target unless rules.fragment == "" && target.recursive_anchor?
346
349
 
347
350
  @dynamic_scope&.each do |resource|
351
+ @budget&.checkpoint
348
352
  compiled = @compiler.compile(resource.root)
349
353
  return compiled if compiled.recursive_anchor?
350
354
  end
@@ -359,6 +363,7 @@ module Schemurai
359
363
  return target unless target.dynamic_anchor == fragment
360
364
 
361
365
  Array(@dynamic_scope).each do |resource|
366
+ @budget&.checkpoint
362
367
  dynamic = @graph.dynamic_anchor(resource, fragment)
363
368
  return @compiler.compile(dynamic) if dynamic
364
369
  end
@@ -560,6 +565,7 @@ module Schemurai
560
565
 
561
566
  if (prefix_items = rules.prefix_items)
562
567
  prefix_items.each_with_index do |child, index|
568
+ @budget&.checkpoint
563
569
  break if index >= length
564
570
  return false unless evaluate_valid(child, value[index])
565
571
  end
@@ -568,12 +574,14 @@ module Schemurai
568
574
  items = rules.items
569
575
  if rules.items_list
570
576
  items.each_with_index do |child, index|
577
+ @budget&.checkpoint
571
578
  break if index >= length
572
579
  return false unless evaluate_valid(child, value[index])
573
580
  end
574
581
  if length > items.length && (additional = rules.additional)
575
582
  index = items.length
576
583
  while index < length
584
+ @budget&.checkpoint
577
585
  return false unless evaluate_valid(additional, value[index])
578
586
  index += 1
579
587
  end
@@ -582,6 +590,7 @@ module Schemurai
582
590
  start = rules.prefix_items&.length || 0
583
591
  index = start
584
592
  while index < length
593
+ @budget&.checkpoint
585
594
  return false unless evaluate_valid(items, value[index])
586
595
  index += 1
587
596
  end
@@ -618,6 +627,7 @@ module Schemurai
618
627
 
619
628
  if (prefix_items = rules.prefix_items)
620
629
  prefix_items.each_with_index do |child, index|
630
+ @budget&.checkpoint
621
631
  break if index >= value.length
622
632
  valid = evaluate_child_at(child, value[index], index, "prefixItems", index)
623
633
  return Evaluation.invalid if !valid && !@errors
@@ -628,6 +638,7 @@ module Schemurai
628
638
  items = rules.items
629
639
  if rules.items_list
630
640
  items.each_with_index do |child, index|
641
+ @budget&.checkpoint
631
642
  break if index >= value.length
632
643
  valid = evaluate_child_at(child, value[index], index, "items", index)
633
644
  return Evaluation.invalid if !valid && !@errors
@@ -635,6 +646,7 @@ module Schemurai
635
646
  end
636
647
  if value.length > items.length && (additional = rules.additional)
637
648
  (items.length...value.length).each do |index|
649
+ @budget&.checkpoint
638
650
  valid = evaluate_child_at(additional, value[index], index, "additionalItems")
639
651
  return Evaluation.invalid if !valid && !@errors
640
652
  (evaluation ||= tracked_evaluation).record_item(index)
@@ -643,6 +655,7 @@ module Schemurai
643
655
  elsif items
644
656
  start = rules.prefix_items&.length || 0
645
657
  (start...value.length).each do |index|
658
+ @budget&.checkpoint
646
659
  valid = evaluate_child_at(items, value[index], index, "items")
647
660
  return Evaluation.invalid if !valid && !@errors
648
661
  (evaluation ||= tracked_evaluation).record_item(index)
@@ -653,6 +666,7 @@ module Schemurai
653
666
  matched = nil
654
667
  index = 0
655
668
  while index < value.length
669
+ @budget&.checkpoint
656
670
  (matched ||= []) << index if trial(contains, value[index]).valid?
657
671
  index += 1
658
672
  end
@@ -673,6 +687,7 @@ module Schemurai
673
687
  prior_items = prior_evaluation.evaluated_items
674
688
  index = 0
675
689
  while index < value.length
690
+ @budget&.checkpoint
676
691
  if prior_items.include?(index) || evaluation&.evaluated_items&.include?(index)
677
692
  index += 1
678
693
  next
@@ -692,7 +707,10 @@ module Schemurai
692
707
  length = value.length
693
708
  return false if rules.max_properties && length > rules.max_properties
694
709
  return false if rules.min_properties && length < rules.min_properties
695
- if rules.required && !rules.required.all? { |name| value.key?(name) }
710
+ if rules.required && !rules.required.all? do |name|
711
+ @budget&.checkpoint
712
+ value.key?(name)
713
+ end
696
714
  return false
697
715
  end
698
716
 
@@ -701,6 +719,7 @@ module Schemurai
701
719
  additional = rules.additional
702
720
  unless properties.empty? && patterns.nil? && additional.nil?
703
721
  value.each do |name, property_value|
722
+ @budget&.checkpoint
704
723
  matched = false
705
724
  if (child = properties[name])
706
725
  matched = true
@@ -708,6 +727,7 @@ module Schemurai
708
727
  end
709
728
  if patterns
710
729
  patterns.each do |pattern, child|
730
+ @budget&.checkpoint
711
731
  next unless ecma_regexp(pattern).match?(name)
712
732
  matched = true
713
733
  return false unless evaluate_valid(child, property_value)
@@ -722,9 +742,13 @@ module Schemurai
722
742
  end
723
743
  if (dependencies = rules.dependencies)
724
744
  dependencies.each do |name, dependency|
745
+ @budget&.checkpoint
725
746
  next unless value.key?(name)
726
747
  if dependency.is_a?(Array)
727
- return false unless dependency.all? { |required_name| value.key?(required_name) }
748
+ return false unless dependency.all? do |required_name|
749
+ @budget&.checkpoint
750
+ value.key?(required_name)
751
+ end
728
752
  else
729
753
  return false unless evaluate_valid(dependency, value)
730
754
  end
@@ -732,12 +756,17 @@ module Schemurai
732
756
  end
733
757
  if (dependent_required = rules.dependent_required)
734
758
  dependent_required.each do |name, required_names|
759
+ @budget&.checkpoint
735
760
  next unless value.key?(name)
736
- return false unless required_names.all? { |required_name| value.key?(required_name) }
761
+ return false unless required_names.all? do |required_name|
762
+ @budget&.checkpoint
763
+ value.key?(required_name)
764
+ end
737
765
  end
738
766
  end
739
767
  if (dependent_schemas = rules.dependent_schemas)
740
768
  dependent_schemas.each do |name, child|
769
+ @budget&.checkpoint
741
770
  next unless value.key?(name)
742
771
  return false unless evaluate_valid(child, value)
743
772
  end
@@ -763,6 +792,7 @@ module Schemurai
763
792
  end
764
793
  if (required = rules.required)
765
794
  required.each do |name|
795
+ @budget&.checkpoint
766
796
  next if value.key?(name)
767
797
  return Evaluation.invalid unless @errors
768
798
 
@@ -773,6 +803,7 @@ module Schemurai
773
803
  properties = rules.properties
774
804
  patterns = rules.patterns
775
805
  value.each do |name, property_value|
806
+ @budget&.checkpoint
776
807
  matched = false
777
808
  if (child = properties[name])
778
809
  matched = true
@@ -782,6 +813,7 @@ module Schemurai
782
813
  end
783
814
  if patterns
784
815
  patterns.each do |pattern, child|
816
+ @budget&.checkpoint
785
817
  next unless ecma_regexp(pattern).match?(name)
786
818
  matched = true
787
819
  valid = evaluate_child_at(child, property_value, name, "patternProperties", pattern)
@@ -798,15 +830,18 @@ module Schemurai
798
830
 
799
831
  if (property_names = rules.property_names)
800
832
  value.each_key do |name|
833
+ @budget&.checkpoint
801
834
  valid = evaluate_child_at(property_names, name, name, "propertyNames")
802
835
  return Evaluation.invalid if !valid && !@errors
803
836
  end
804
837
  end
805
838
  if (dependencies = rules.dependencies)
806
839
  dependencies.each do |name, dependency|
840
+ @budget&.checkpoint
807
841
  next unless value.key?(name)
808
842
  if dependency.is_a?(Array)
809
843
  dependency.each do |required_name|
844
+ @budget&.checkpoint
810
845
  unless value.key?(required_name)
811
846
  return Evaluation.invalid unless @errors
812
847
 
@@ -817,6 +852,7 @@ module Schemurai
817
852
  result = evaluate_at(dependency, value, MISSING_SEGMENT, "dependencies", name)
818
853
  if result.valid? && !result.evaluated_properties.empty?
819
854
  result.evaluated_properties.each do |property|
855
+ @budget&.checkpoint
820
856
  (evaluation ||= tracked_evaluation).record_property(property)
821
857
  end
822
858
  end
@@ -825,8 +861,10 @@ module Schemurai
825
861
  end
826
862
  if (dependent_required = rules.dependent_required)
827
863
  dependent_required.each do |name, required_names|
864
+ @budget&.checkpoint
828
865
  next unless value.key?(name)
829
866
  required_names.each do |required_name|
867
+ @budget&.checkpoint
830
868
  unless value.key?(required_name)
831
869
  return Evaluation.invalid unless @errors
832
870
 
@@ -837,10 +875,12 @@ module Schemurai
837
875
  end
838
876
  if (dependent_schemas = rules.dependent_schemas)
839
877
  dependent_schemas.each do |name, child|
878
+ @budget&.checkpoint
840
879
  next unless value.key?(name)
841
880
  result = evaluate_at(child, value, MISSING_SEGMENT, "dependentSchemas", name)
842
881
  if result.valid? && !result.evaluated_properties.empty?
843
882
  result.evaluated_properties.each do |property|
883
+ @budget&.checkpoint
844
884
  (evaluation ||= tracked_evaluation).record_property(property)
845
885
  end
846
886
  end
@@ -849,6 +889,7 @@ module Schemurai
849
889
  if (unevaluated = rules.unevaluated)
850
890
  prior_properties = prior_evaluation.evaluated_properties
851
891
  value.each_key do |name|
892
+ @budget&.checkpoint
852
893
  next if prior_properties.include?(name) || evaluation&.evaluated_properties&.include?(name)
853
894
 
854
895
  valid = evaluate_child_at(unevaluated, value[name], name, "unevaluatedProperties")
@@ -867,6 +908,7 @@ module Schemurai
867
908
  when :allOf
868
909
  index = 0
869
910
  while index < operand.length
911
+ @budget&.checkpoint
870
912
  evaluation = evaluation.merge(evaluate_at(operand[index], value, MISSING_SEGMENT, "allOf", index))
871
913
  index += 1
872
914
  end
@@ -874,6 +916,7 @@ module Schemurai
874
916
  matches = 0
875
917
  index = 0
876
918
  while index < operand.length
919
+ @budget&.checkpoint
877
920
  result = trial(operand[index], value)
878
921
  if result.valid?
879
922
  matches += 1
@@ -887,6 +930,7 @@ module Schemurai
887
930
  matched_evaluation = nil
888
931
  index = 0
889
932
  while index < operand.length
933
+ @budget&.checkpoint
890
934
  result = trial(operand[index], value)
891
935
  if result.valid?
892
936
  matches += 1
@@ -989,6 +1033,7 @@ module Schemurai
989
1033
  case left
990
1034
  when Hash
991
1035
  left.length == right.length && left.all? do |key, value|
1036
+ @budget&.checkpoint
992
1037
  right.key?(key) && json_equal?(value, right[key])
993
1038
  end
994
1039
  when Array
@@ -996,6 +1041,7 @@ module Schemurai
996
1041
 
997
1042
  index = 0
998
1043
  while index < left.length
1044
+ @budget&.checkpoint
999
1045
  return false unless json_equal?(left[index], right[index])
1000
1046
  index += 1
1001
1047
  end
@@ -1024,6 +1070,7 @@ module Schemurai
1024
1070
  escaped = false
1025
1071
  in_class = false
1026
1072
  pattern.each_char do |character|
1073
+ @budget&.checkpoint
1027
1074
  if escaped
1028
1075
  translated << case character
1029
1076
  when "d" then in_class ? "0-9" : "[0-9]"
@@ -0,0 +1,97 @@
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 VM
9
+ # VM execution with invocation-local limits; no Ruby evaluator delegation.
10
+ class LimitedEvaluator < Evaluator
11
+ def validate(instance, max_errors: nil, timeout: nil, budget: nil)
12
+ @budget = budget || Internal::LimitBudget.new(max_errors: max_errors, timeout: timeout)
13
+ result = super(instance)
14
+ @budget.check_deadline!
15
+ result
16
+ ensure
17
+ @budget = nil
18
+ end
19
+
20
+ def valid?(instance, max_errors: nil, timeout: nil, budget: nil)
21
+ @budget = budget || Internal::LimitBudget.new(max_errors: max_errors, timeout: timeout)
22
+ result = super(instance)
23
+ @budget.check_deadline!
24
+ result
25
+ ensure
26
+ @budget = nil
27
+ end
28
+
29
+ private def evaluate(node, instance)
30
+ @budget.checkpoint
31
+ super
32
+ end
33
+
34
+ private def evaluate_valid(node, instance)
35
+ @budget.checkpoint
36
+ super
37
+ end
38
+
39
+ private def add_error(keyword, message = nil, append_keyword: true, &block)
40
+ @budget.reserve_error if @errors
41
+ super
42
+ end
43
+
44
+ private def valid_string?(node, value)
45
+ @budget.check_deadline!
46
+ result = super
47
+ @budget.check_deadline!
48
+ result
49
+ end
50
+
51
+ private def check_string(node, value)
52
+ @budget.check_deadline!
53
+ result = super
54
+ @budget.check_deadline!
55
+ result
56
+ end
57
+
58
+ private def decimal(value)
59
+ @budget.check_deadline!
60
+ result = super
61
+ @budget.check_deadline!
62
+ result
63
+ end
64
+
65
+ private def ecma_regexp(pattern)
66
+ @budget.check_deadline!
67
+ result = super
68
+ @budget.check_deadline!
69
+ result
70
+ end
71
+
72
+ private def valid_content?(schema, value)
73
+ @budget.check_deadline!
74
+ result = super
75
+ @budget.check_deadline!
76
+ result
77
+ end
78
+
79
+ private def check_content(schema, value)
80
+ @budget.check_deadline!
81
+ result = super
82
+ @budget.check_deadline!
83
+ result
84
+ end
85
+
86
+ private def json_equal?(left, right)
87
+ @budget.checkpoint
88
+ super
89
+ end
90
+
91
+ private def json_fingerprint(value)
92
+ @budget.checkpoint
93
+ super
94
+ end
95
+ end
96
+ end
97
+ end
data/lib/schemurai.rb CHANGED
@@ -11,6 +11,8 @@ module Schemurai
11
11
  # Base class for errors raised by Schemurai.
12
12
  class Error < StandardError; end
13
13
 
14
+ require_relative "schemurai/limited_evaluator"
15
+
14
16
  # Raised when a schema or URI cannot be resolved from a registry.
15
17
  class ResolutionError < Error; end
16
18
 
@@ -96,27 +98,31 @@ module Schemurai
96
98
  # Compiles +schema+ and returns a Validator.
97
99
  #
98
100
  # +base_uri+ supplies the schema's base URI. Pass +content: true+ or
99
- # +format: true+ to enable the corresponding optional assertions.
100
- def compile(schema, base_uri: nil, content: false, format: false)
101
+ # +format: true+ to enable the corresponding optional assertions. +max_errors:+ and +timeout:+
102
+ # configure per-call validation caps; schema compilation is not bounded.
103
+ def compile(schema, base_uri: nil, content: false, format: false, max_errors: nil, timeout: nil)
101
104
  raise Error, "cannot compile schemas after the registry is made shareable" if shareable?
102
105
 
103
106
  validate_schema!(schema) if validate_schema?
104
107
  root = @graph.compile(schema, base_uri: base_uri)
105
- build_validator(root, content: content, format: format)
108
+ build_validator(root, content: content, format: format, max_errors: max_errors, timeout: timeout)
106
109
  end
107
110
 
108
111
  # Validates +schema+ against its declared meta-schema and returns a Result.
109
112
  # Schemas without +$schema+ are validated as Draft 7.
110
- def validate_schema(schema)
113
+ def validate_schema(schema, max_errors: nil, timeout: nil)
111
114
  raise Error, "cannot validate schemas after the registry is made shareable" if shareable?
112
115
 
113
116
  root = @graph.meta_schema_root(schema)
114
- build_validator(root, content: false, format: false).validate(schema)
117
+ build_validator(root, content: false, format: false, max_errors: max_errors, timeout: timeout).validate(schema)
115
118
  end
116
119
 
117
120
  # Returns whether +schema+ conforms to its declared meta-schema.
118
- def valid_schema?(schema)
119
- validate_schema(schema).valid?
121
+ def valid_schema?(schema, max_errors: nil, timeout: nil)
122
+ raise Error, "cannot validate schemas after the registry is made shareable" if shareable?
123
+
124
+ root = @graph.meta_schema_root(schema)
125
+ build_validator(root, content: false, format: false, max_errors: max_errors, timeout: timeout).valid?(schema)
120
126
  end
121
127
 
122
128
  # Resolves and freezes registered schemas, then makes the registry Ractor-shareable.
@@ -127,6 +133,13 @@ module Schemurai
127
133
 
128
134
  @graph.make_shareable
129
135
  @compiler&.compile_all
136
+ if @backend == :native
137
+ programs = @graph.nodes.map { |node| @compiler.compile(node) }
138
+ # Build every option variant before the registry becomes immutable.
139
+ @native_programs = [false, true].product([false, true]).to_h do |format, content|
140
+ [[format, content], Native::Program.new(programs, @compiler, format, content).make_shareable]
141
+ end
142
+ end
130
143
  Ractor.make_shareable(self)
131
144
  end
132
145
 
@@ -138,24 +151,36 @@ module Schemurai
138
151
  # Returns a Validator for the registered resource at +uri+.
139
152
  #
140
153
  # The registry must first be made shareable with #make_shareable.
141
- def validator_for(uri, content: false, format: false)
154
+ def validator_for(uri, content: false, format: false, max_errors: nil, timeout: nil)
142
155
  raise Error, "make_shareable must be called before retrieving validators by URI" unless shareable?
143
156
 
144
157
  root = @graph.node_at(uri)
145
158
  raise ResolutionError, "unregistered schema URI #{uri.inspect}" unless root
146
159
 
147
- build_validator(root, content: content, format: format)
160
+ build_validator(root, content: content, format: format, max_errors: max_errors, timeout: timeout)
148
161
  end
149
162
 
150
- private def build_validator(root, content:, format:)
163
+ private def build_validator(root, content:, format:, max_errors: nil, timeout: nil)
151
164
  evaluator = if @compiler
152
165
  program = @compiler.compile(root)
153
166
  evaluator_class = (@backend == :native) ? Native::Evaluator : VM::Evaluator
154
- evaluator_class.new(@graph, @compiler, program, content: content, format: format)
167
+ if @native_programs
168
+ compiled = @native_programs.fetch([!!format, !!content])
169
+ Native::Evaluator.new(@graph, @compiler, program, content: content, format: format, compiled: compiled)
170
+ else
171
+ evaluator_class.new(@graph, @compiler, program, content: content, format: format)
172
+ end
155
173
  else
156
174
  Internal::Evaluator.new(@graph, root, content: content, format: format)
157
175
  end
158
- Validator.new(evaluator)
176
+ bounded_evaluator = lambda do
177
+ case @backend
178
+ when :native then evaluator
179
+ when :vm then VM::LimitedEvaluator.new(@graph, @compiler, program, content: content, format: format)
180
+ else Internal::LimitedEvaluator.new(@graph, root, content: content, format: format)
181
+ end
182
+ end
183
+ Validator.new(evaluator, max_errors: max_errors, timeout: timeout, bounded_evaluator: bounded_evaluator)
159
184
  end
160
185
 
161
186
  private def validate_schema!(schema)
@@ -174,8 +199,12 @@ module Schemurai
174
199
  #
175
200
  # Applications should normally create validators through Schemurai.compile
176
201
  # or SchemaRegistry#compile.
177
- def initialize(evaluator)
202
+ def initialize(evaluator, max_errors: nil, timeout: nil, bounded_evaluator: nil)
178
203
  @evaluator = evaluator
204
+ Internal::LimitBudget.validate_options!(max_errors, timeout)
205
+ @max_errors = max_errors
206
+ @timeout = timeout
207
+ @bounded_evaluator = bounded_evaluator
179
208
  end
180
209
 
181
210
  # The backend reported by the evaluator, +:ruby+, +:vm+, or +:native+.
@@ -184,13 +213,28 @@ module Schemurai
184
213
  end
185
214
 
186
215
  # Validates +instance+ and returns a Result containing all reported errors.
187
- def validate(instance)
188
- @evaluator.validate(instance)
216
+ # +max_errors:+ and +timeout:+ override compiled defaults. Raises LimitExceeded when evaluation
217
+ # cannot complete; no partial Result is returned.
218
+ def validate(instance, max_errors: @max_errors, timeout: @timeout)
219
+ Internal::LimitBudget.validate_options!(max_errors, timeout)
220
+ return @evaluator.validate(instance) if max_errors.nil? && timeout.nil?
221
+
222
+ limited_evaluator.validate(instance, max_errors: max_errors, timeout: timeout)
189
223
  end
190
224
 
191
225
  # Returns whether +instance+ is valid without building a detailed error list.
192
- def valid?(instance)
193
- @evaluator.valid?(instance)
226
+ # The timeout applies; the diagnostic count limit is ignored.
227
+ def valid?(instance, max_errors: @max_errors, timeout: @timeout)
228
+ Internal::LimitBudget.validate_options!(max_errors, timeout)
229
+ return @evaluator.valid?(instance) if timeout.nil?
230
+
231
+ limited_evaluator.valid?(instance, timeout: timeout)
232
+ end
233
+
234
+ private def limited_evaluator
235
+ raise ArgumentError, "this manually constructed Validator has no bounded evaluator" unless @bounded_evaluator
236
+
237
+ @bounded_evaluator.call
194
238
  end
195
239
  end
196
240
 
@@ -204,31 +248,32 @@ module Schemurai
204
248
  # +schemas+ maps external URI strings to schemas. +base_uri+ supplies the
205
249
  # schema's base URI. Optional content and format assertions are disabled by
206
250
  # default. +backend+ accepts +:ruby+, +:vm+, +:native+, or +:default+.
207
- module_function def compile(schema, schemas: {}, base_uri: nil, content: false, format: false, validate_schema: false, backend: Backend.requested)
251
+ module_function def compile(schema, schemas: {}, base_uri: nil, content: false, format: false, validate_schema: false, backend: Backend.requested, max_errors: nil, timeout: nil)
208
252
  SchemaRegistry.new(schemas: schemas, validate_schema: validate_schema, backend: backend).compile(
209
253
  schema,
210
254
  base_uri: base_uri,
211
255
  content: content,
212
- format: format
256
+ format: format,
257
+ max_errors: max_errors, timeout: timeout
213
258
  )
214
259
  end
215
260
 
216
261
  # Validates +schema+ against its declared meta-schema and returns a Result.
217
262
  # Schemas without +$schema+ are validated as Draft 7.
218
- module_function def validate_schema(schema, schemas: {}, backend: Backend.requested)
219
- SchemaRegistry.new(schemas: schemas, backend: backend).validate_schema(schema)
263
+ module_function def validate_schema(schema, schemas: {}, backend: Backend.requested, max_errors: nil, timeout: nil)
264
+ SchemaRegistry.new(schemas: schemas, backend: backend).validate_schema(schema, max_errors: max_errors, timeout: timeout)
220
265
  end
221
266
 
222
267
  # Returns whether +schema+ conforms to its declared meta-schema.
223
- module_function def valid_schema?(schema, schemas: {}, backend: Backend.requested)
224
- validate_schema(schema, schemas: schemas, backend: backend).valid?
268
+ module_function def valid_schema?(schema, schemas: {}, backend: Backend.requested, max_errors: nil, timeout: nil)
269
+ SchemaRegistry.new(schemas: schemas, backend: backend).valid_schema?(schema, max_errors: max_errors, timeout: timeout)
225
270
  end
226
271
 
227
272
  # Validates +instance+ against +schema+ and returns a Result.
228
273
  #
229
274
  # This convenience method compiles the schema for each call. Use .compile
230
275
  # when validating multiple instances against the same schema.
231
- module_function def validate(schema, instance, schemas: {}, base_uri: nil, content: false, format: false, validate_schema: false, backend: Backend.requested)
276
+ module_function def validate(schema, instance, schemas: {}, base_uri: nil, content: false, format: false, validate_schema: false, backend: Backend.requested, max_errors: nil, timeout: nil)
232
277
  compile(
233
278
  schema,
234
279
  schemas: schemas,
@@ -236,7 +281,8 @@ module Schemurai
236
281
  content: content,
237
282
  format: format,
238
283
  validate_schema: validate_schema,
239
- backend: backend
284
+ backend: backend,
285
+ max_errors: max_errors, timeout: timeout
240
286
  ).validate(instance)
241
287
  end
242
288
 
@@ -244,7 +290,7 @@ module Schemurai
244
290
  #
245
291
  # This convenience method compiles the schema for each call. Use .compile
246
292
  # when validating multiple instances against the same schema.
247
- module_function def valid?(schema, instance, schemas: {}, base_uri: nil, content: false, format: false, validate_schema: false, backend: Backend.requested)
293
+ module_function def valid?(schema, instance, schemas: {}, base_uri: nil, content: false, format: false, validate_schema: false, backend: Backend.requested, max_errors: nil, timeout: nil)
248
294
  compile(
249
295
  schema,
250
296
  schemas: schemas,
@@ -252,7 +298,8 @@ module Schemurai
252
298
  content: content,
253
299
  format: format,
254
300
  validate_schema: validate_schema,
255
- backend: backend
301
+ backend: backend,
302
+ max_errors: max_errors, timeout: timeout
256
303
  ).valid?(instance)
257
304
  end
258
305
 
data/sig/schemurai.rbs CHANGED
@@ -19,6 +19,14 @@ module Schemurai
19
19
  class Error < StandardError
20
20
  end
21
21
 
22
+ class LimitExceeded < Error
23
+ attr_reader limit: Symbol
24
+ attr_reader maximum: Integer | Float
25
+ attr_reader attempted: Integer | Float
26
+
27
+ def initialize: (Symbol limit, Integer | Float maximum, Integer | Float attempted) -> void
28
+ end
29
+
22
30
  class ResolutionError < Error
23
31
  end
24
32
 
@@ -63,10 +71,10 @@ module Schemurai
63
71
  end
64
72
 
65
73
  class Validator
66
- def initialize: (_Evaluator evaluator) -> void
74
+ def initialize: (_Evaluator evaluator, ?max_errors: Integer?, ?timeout: (Integer | Float)?, ?bounded_evaluator: (^() -> untyped)?) -> void
67
75
  def backend: () -> backend
68
- def validate: (json_value instance) -> Result
69
- def valid?: (json_value instance) -> bool
76
+ def validate: (json_value instance, ?max_errors: Integer?, ?timeout: (Integer | Float)?) -> Result
77
+ def valid?: (json_value instance, ?max_errors: Integer?, ?timeout: (Integer | Float)?) -> bool
70
78
  end
71
79
 
72
80
  class SchemaRegistry
@@ -76,25 +84,25 @@ module Schemurai
76
84
 
77
85
  def validate_schema?: () -> bool
78
86
 
79
- def compile: (schema schema, ?base_uri: String?, ?content: bool, ?format: bool) -> Validator
87
+ def compile: (schema schema, ?base_uri: String?, ?content: bool, ?format: bool, ?max_errors: Integer?, ?timeout: (Integer | Float)?) -> Validator
80
88
 
81
- def validate_schema: (schema schema) -> Result
82
- def valid_schema?: (schema schema) -> bool
89
+ def validate_schema: (schema schema, ?max_errors: Integer?, ?timeout: (Integer | Float)?) -> Result
90
+ def valid_schema?: (schema schema, ?max_errors: Integer?, ?timeout: (Integer | Float)?) -> bool
83
91
  def make_shareable: () -> self
84
92
  def shareable?: () -> bool
85
93
 
86
- def validator_for: (String uri, ?content: bool, ?format: bool) -> Validator
94
+ def validator_for: (String uri, ?content: bool, ?format: bool, ?max_errors: Integer?, ?timeout: (Integer | Float)?) -> Validator
87
95
  end
88
96
 
89
97
  def self.backend: () -> backend
90
98
 
91
- def self.compile: (schema schema, ?schemas: schemas, ?base_uri: String?, ?content: bool, ?format: bool, ?validate_schema: bool, ?backend: backend_selection) -> Validator
99
+ def self.compile: (schema schema, ?schemas: schemas, ?base_uri: String?, ?content: bool, ?format: bool, ?validate_schema: bool, ?backend: backend_selection, ?max_errors: Integer?, ?timeout: (Integer | Float)?) -> Validator
92
100
 
93
- def self.validate_schema: (schema schema, ?schemas: schemas, ?backend: backend_selection) -> Result
101
+ def self.validate_schema: (schema schema, ?schemas: schemas, ?backend: backend_selection, ?max_errors: Integer?, ?timeout: (Integer | Float)?) -> Result
94
102
 
95
- def self.valid_schema?: (schema schema, ?schemas: schemas, ?backend: backend_selection) -> bool
103
+ def self.valid_schema?: (schema schema, ?schemas: schemas, ?backend: backend_selection, ?max_errors: Integer?, ?timeout: (Integer | Float)?) -> bool
96
104
 
97
- def self.validate: (schema schema, json_value instance, ?schemas: schemas, ?base_uri: String?, ?content: bool, ?format: bool, ?validate_schema: bool, ?backend: backend_selection) -> Result
105
+ def self.validate: (schema schema, json_value instance, ?schemas: schemas, ?base_uri: String?, ?content: bool, ?format: bool, ?validate_schema: bool, ?backend: backend_selection, ?max_errors: Integer?, ?timeout: (Integer | Float)?) -> Result
98
106
 
99
- def self.valid?: (schema schema, json_value instance, ?schemas: schemas, ?base_uri: String?, ?content: bool, ?format: bool, ?validate_schema: bool, ?backend: backend_selection) -> bool
107
+ def self.valid?: (schema schema, json_value instance, ?schemas: schemas, ?base_uri: String?, ?content: bool, ?format: bool, ?validate_schema: bool, ?backend: backend_selection, ?max_errors: Integer?, ?timeout: (Integer | Float)?) -> bool
100
108
  end