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,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Internal
5
+ class Dialect
6
+ TYPE = 1
7
+ ENUM = 2
8
+ COMBINER = 4
9
+ NUMBER = 8
10
+ STRING = 16
11
+ ARRAY = 32
12
+ OBJECT = 64
13
+
14
+ Keyword = Data.define(:mask, :subschema_shape) do
15
+ def initialize(mask:, subschema_shape: nil)
16
+ super
17
+ end
18
+ end
19
+
20
+ class << self
21
+ def register(dialect, default: false)
22
+ registry[normalize_uri(dialect.uri)] = dialect
23
+ @default = dialect if default
24
+ dialect
25
+ end
26
+
27
+ def resolve(uri = nil)
28
+ return @default if uri.nil?
29
+
30
+ registry[normalize_uri(uri)]
31
+ end
32
+
33
+ private def registry
34
+ @registry ||= {}
35
+ end
36
+
37
+ private def normalize_uri(uri)
38
+ uri.to_s.delete_suffix("#")
39
+ end
40
+ end
41
+
42
+ attr_reader :name, :uri, :keywords
43
+
44
+ def initialize(name:, uri:, keywords:, ref_siblings:, format_assertion: false)
45
+ @name = name
46
+ @uri = uri
47
+ @keywords = keywords.freeze
48
+ @ref_siblings = ref_siblings
49
+ @format_assertion = format_assertion
50
+ freeze
51
+ end
52
+
53
+ def ref_siblings?
54
+ @ref_siblings
55
+ end
56
+
57
+ def format_assertion?
58
+ @format_assertion
59
+ end
60
+
61
+ def keyword_mask(schema)
62
+ schema.each_key.reduce(0) do |mask, keyword|
63
+ mask | (keywords[keyword]&.mask || 0)
64
+ end
65
+ end
66
+
67
+ def each_subschema(schema)
68
+ return enum_for(__method__, schema) unless block_given?
69
+ return unless schema.is_a?(Hash)
70
+ return if schema.key?("$ref") && !ref_siblings?
71
+
72
+ schema.each do |keyword, value|
73
+ specification = keywords[keyword]
74
+ next unless specification&.subschema_shape
75
+
76
+ case specification.subschema_shape
77
+ when :single
78
+ yield value, [keyword] if schema?(value)
79
+ when :single_or_list
80
+ if value.is_a?(Array)
81
+ value.each_with_index { |child, index| yield child, [keyword, index] if schema?(child) }
82
+ elsif schema?(value)
83
+ yield value, [keyword]
84
+ end
85
+ when :list
86
+ Array(value).each_with_index { |child, index| yield child, [keyword, index] if schema?(child) }
87
+ when :map
88
+ value.each { |name, child| yield child, [keyword, name] if schema?(child) } if value.is_a?(Hash)
89
+ when :dependencies
90
+ if value.is_a?(Hash)
91
+ value.each do |name, child|
92
+ yield child, [keyword, name] if schema?(child)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ private def schema?(value)
100
+ value == true || value == false || value.is_a?(Hash)
101
+ end
102
+ end
103
+ end
104
+
105
+ private_constant :Internal
106
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dialect"
4
+
5
+ module Schemurai
6
+ module Internal
7
+ module DialectKeywords
8
+ COMMON = {
9
+ "type" => Dialect::Keyword.new(mask: Dialect::TYPE),
10
+ "enum" => Dialect::Keyword.new(mask: Dialect::ENUM),
11
+ "const" => Dialect::Keyword.new(mask: Dialect::ENUM),
12
+ "allOf" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :list),
13
+ "anyOf" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :list),
14
+ "oneOf" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :list),
15
+ "not" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :single),
16
+ "if" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :single),
17
+ "then" => Dialect::Keyword.new(mask: 0, subschema_shape: :single),
18
+ "else" => Dialect::Keyword.new(mask: 0, subschema_shape: :single),
19
+ "maximum" => Dialect::Keyword.new(mask: Dialect::NUMBER),
20
+ "minimum" => Dialect::Keyword.new(mask: Dialect::NUMBER),
21
+ "exclusiveMaximum" => Dialect::Keyword.new(mask: Dialect::NUMBER),
22
+ "exclusiveMinimum" => Dialect::Keyword.new(mask: Dialect::NUMBER),
23
+ "multipleOf" => Dialect::Keyword.new(mask: Dialect::NUMBER),
24
+ "maxLength" => Dialect::Keyword.new(mask: Dialect::STRING),
25
+ "minLength" => Dialect::Keyword.new(mask: Dialect::STRING),
26
+ "pattern" => Dialect::Keyword.new(mask: Dialect::STRING),
27
+ "contentEncoding" => Dialect::Keyword.new(mask: Dialect::STRING),
28
+ "contentMediaType" => Dialect::Keyword.new(mask: Dialect::STRING),
29
+ "maxItems" => Dialect::Keyword.new(mask: Dialect::ARRAY),
30
+ "minItems" => Dialect::Keyword.new(mask: Dialect::ARRAY),
31
+ "uniqueItems" => Dialect::Keyword.new(mask: Dialect::ARRAY),
32
+ "contains" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single),
33
+ "maxProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT),
34
+ "minProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT),
35
+ "required" => Dialect::Keyword.new(mask: Dialect::OBJECT),
36
+ "properties" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :map),
37
+ "patternProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :map),
38
+ "additionalProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :single),
39
+ "propertyNames" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :single)
40
+ }.freeze
41
+
42
+ LEGACY = {
43
+ "dependencies" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :dependencies),
44
+ "definitions" => Dialect::Keyword.new(mask: 0, subschema_shape: :map)
45
+ }.freeze
46
+
47
+ MODERN = {
48
+ "$defs" => Dialect::Keyword.new(mask: 0, subschema_shape: :map),
49
+ "$recursiveRef" => Dialect::Keyword.new(mask: Dialect::COMBINER),
50
+ "dependentSchemas" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :map),
51
+ "dependentRequired" => Dialect::Keyword.new(mask: Dialect::OBJECT),
52
+ "minContains" => Dialect::Keyword.new(mask: Dialect::ARRAY),
53
+ "maxContains" => Dialect::Keyword.new(mask: Dialect::ARRAY),
54
+ "unevaluatedItems" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single),
55
+ "unevaluatedProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :single),
56
+ "contentSchema" => Dialect::Keyword.new(mask: Dialect::STRING, subschema_shape: :single)
57
+ }.freeze
58
+
59
+ private_constant :COMMON, :LEGACY, :MODERN
60
+
61
+ module_function def draft7
62
+ COMMON.merge(
63
+ LEGACY,
64
+ "items" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single_or_list),
65
+ "additionalItems" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single)
66
+ ).freeze
67
+ end
68
+
69
+ module_function def draft2019_09
70
+ COMMON.merge(
71
+ LEGACY,
72
+ MODERN,
73
+ "items" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single_or_list),
74
+ "additionalItems" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single)
75
+ ).freeze
76
+ end
77
+
78
+ module_function def draft2020_12
79
+ COMMON.merge(
80
+ LEGACY,
81
+ MODERN,
82
+ "$dynamicRef" => Dialect::Keyword.new(mask: Dialect::COMBINER),
83
+ "prefixItems" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :list),
84
+ "items" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single)
85
+ ).freeze
86
+ end
87
+ end
88
+ end
89
+
90
+ private_constant :Internal
91
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../dialect_keywords"
4
+
5
+ module Schemurai
6
+ module Internal
7
+ module Dialects
8
+ module Draft201909
9
+ META_SCHEMA_URI = "https://json-schema.org/draft/2019-09/schema"
10
+ KEYWORDS = DialectKeywords.draft2019_09
11
+
12
+ DIALECT = Dialect.new(
13
+ name: :draft2019_09,
14
+ uri: META_SCHEMA_URI,
15
+ keywords: KEYWORDS,
16
+ ref_siblings: true
17
+ )
18
+
19
+ Dialect.register(DIALECT)
20
+
21
+ private_constant :META_SCHEMA_URI, :KEYWORDS
22
+ end
23
+ end
24
+ end
25
+
26
+ private_constant :Internal
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../dialect_keywords"
4
+
5
+ module Schemurai
6
+ module Internal
7
+ module Dialects
8
+ module Draft202012
9
+ META_SCHEMA_URI = "https://json-schema.org/draft/2020-12/schema"
10
+ KEYWORDS = DialectKeywords.draft2020_12
11
+
12
+ DIALECT = Dialect.new(
13
+ name: :draft2020_12,
14
+ uri: META_SCHEMA_URI,
15
+ keywords: KEYWORDS,
16
+ ref_siblings: true
17
+ )
18
+
19
+ Dialect.register(DIALECT)
20
+
21
+ private_constant :META_SCHEMA_URI, :KEYWORDS
22
+ end
23
+ end
24
+ end
25
+
26
+ private_constant :Internal
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../dialect_keywords"
4
+
5
+ module Schemurai
6
+ module Internal
7
+ module Dialects
8
+ module Draft7
9
+ META_SCHEMA_URI = "http://json-schema.org/draft-07/schema"
10
+ KEYWORDS = DialectKeywords.draft7
11
+
12
+ DIALECT = Dialect.new(
13
+ name: :draft7,
14
+ uri: META_SCHEMA_URI,
15
+ keywords: KEYWORDS,
16
+ ref_siblings: false
17
+ )
18
+
19
+ Dialect.register(DIALECT, default: true)
20
+
21
+ private_constant :META_SCHEMA_URI, :KEYWORDS
22
+ end
23
+ end
24
+ end
25
+
26
+ private_constant :Internal
27
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Internal
5
+ # Shared ECMA character classes and absolute anchors for Ruby's Regexp.
6
+ module EcmaRegexp
7
+ module_function def compile(pattern, budget = nil)
8
+ whitespace = "\\u0009-\\u000D\\u0020\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000\\uFEFF"
9
+ translated = +""
10
+ escaped = false
11
+ in_class = false
12
+ pattern.each_char do |character|
13
+ budget&.checkpoint
14
+ if escaped
15
+ translated << case character
16
+ when "d" then in_class ? "0-9" : "[0-9]"
17
+ when "D" then in_class ? "^0-9" : "[^0-9]"
18
+ when "w" then in_class ? "A-Za-z0-9_" : "[A-Za-z0-9_]"
19
+ when "W" then in_class ? "^A-Za-z0-9_" : "[^A-Za-z0-9_]"
20
+ when "s" then in_class ? whitespace : "[#{whitespace}]"
21
+ when "S" then in_class ? "^#{whitespace}" : "[^#{whitespace}]"
22
+ else "\\#{character}"
23
+ end
24
+ escaped = false
25
+ elsif character == "\\"
26
+ escaped = true
27
+ elsif character == "["
28
+ in_class = true
29
+ translated << character
30
+ elsif character == "]"
31
+ in_class = false
32
+ translated << character
33
+ elsif character == "^" && !in_class
34
+ translated << "\\A"
35
+ elsif character == "$" && !in_class
36
+ translated << "\\z"
37
+ else
38
+ translated << character
39
+ end
40
+ end
41
+ translated << "\\" if escaped
42
+ Regexp.new(translated)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Internal
5
+ # Builds validation messages shared by every evaluator backend.
6
+ module ErrorMessage
7
+ module_function def false_schema = "value is not allowed by this schema"
8
+
9
+ module_function def type(expected, value)
10
+ expected = Array(expected).map { |name| name.inspect }.join(" or ")
11
+ "value has type #{json_type(value).inspect}, but must have type #{expected}"
12
+ end
13
+
14
+ module_function def enum = "value must equal one of the values defined by `enum`"
15
+ module_function def const = "value must equal the value defined by `const`"
16
+
17
+ module_function def numeric_limit(keyword, limit)
18
+ comparison = {
19
+ "maximum" => "less than or equal to",
20
+ "minimum" => "greater than or equal to",
21
+ "exclusiveMaximum" => "less than",
22
+ "exclusiveMinimum" => "greater than"
23
+ }.fetch(keyword)
24
+ "number must be #{comparison} #{display_number(limit)}"
25
+ end
26
+
27
+ module_function def multiple_of(divisor) = "number must be a multiple of #{display_number(divisor)}"
28
+
29
+ module_function def size(keyword, limit, actual)
30
+ subject, unit, comparison = {
31
+ "maxLength" => ["string", "characters", "at most"],
32
+ "minLength" => ["string", "characters", "at least"],
33
+ "maxItems" => ["array", "items", "at most"],
34
+ "minItems" => ["array", "items", "at least"],
35
+ "maxProperties" => ["object", "properties", "at most"],
36
+ "minProperties" => ["object", "properties", "at least"]
37
+ }.fetch(keyword)
38
+ unit = {"characters" => "character", "items" => "item", "properties" => "property"}.fetch(unit) if limit == 1
39
+ "#{subject} must contain #{comparison} #{limit} #{unit} (found #{actual})"
40
+ end
41
+
42
+ module_function def pattern(pattern) = "string must match pattern #{pattern.inspect}"
43
+ module_function def invalid_pattern(pattern) = "schema pattern #{pattern.inspect} is not a valid regular expression"
44
+ module_function def format(name) = "string must match the #{name} format"
45
+ module_function def content_encoding = "string must be valid base64"
46
+ module_function def content_media_type = "string must contain valid JSON"
47
+ module_function def unique_items = "array items must be unique"
48
+
49
+ module_function def contains(actual, minimum, maximum)
50
+ expected = if maximum.infinite?
51
+ "at least #{minimum}"
52
+ elsif minimum == maximum
53
+ "exactly #{minimum}"
54
+ elsif minimum.zero?
55
+ "at most #{maximum}"
56
+ else
57
+ "between #{minimum} and #{maximum}"
58
+ end
59
+ "array must contain #{expected} items matching `contains` (found #{actual})"
60
+ end
61
+
62
+ module_function def required(name) = "object is missing required property #{name.inspect}"
63
+
64
+ module_function def dependent_required(name, required_name)
65
+ "property #{required_name.inspect} is required when property #{name.inspect} is present"
66
+ end
67
+
68
+ module_function def any_of = "value must match at least one subschema"
69
+ module_function def one_of(matches) = "value must match exactly one subschema (matched #{matches})"
70
+ module_function def not = "value must not match the subschema"
71
+
72
+ module_function def json_type(value)
73
+ case value
74
+ when nil then "null"
75
+ when true, false then "boolean"
76
+ when Hash then "object"
77
+ when Array then "array"
78
+ when String then "string"
79
+ when Numeric
80
+ if value.is_a?(Complex)
81
+ value.class.name
82
+ elsif value.finite? && value.to_i == value
83
+ "integer"
84
+ else
85
+ "number"
86
+ end
87
+ else
88
+ value.class.name
89
+ end
90
+ end
91
+ private_class_method :json_type
92
+
93
+ module_function def display_number(value)
94
+ rational = value.is_a?(Rational) ? value : Rational(value.to_s)
95
+ return rational.numerator.to_s if rational.denominator == 1
96
+
97
+ rational.to_f.to_s
98
+ end
99
+ private_class_method :display_number
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ # Internal result of evaluating one schema location. Draft 7 only needs the
5
+ # validity bit, but newer drafts also propagate the instance locations
6
+ # consumed by applicators for unevaluatedProperties and unevaluatedItems.
7
+ class Evaluation
8
+ EMPTY_LOCATIONS = [].freeze
9
+
10
+ attr_reader :evaluated_properties, :evaluated_items
11
+
12
+ def self.valid(evaluated_properties: EMPTY_LOCATIONS, evaluated_items: EMPTY_LOCATIONS)
13
+ if evaluated_properties.equal?(EMPTY_LOCATIONS) && evaluated_items.equal?(EMPTY_LOCATIONS)
14
+ return @valid ||= new(true, EMPTY_LOCATIONS, EMPTY_LOCATIONS)
15
+ end
16
+
17
+ new(true, evaluated_properties, evaluated_items)
18
+ end
19
+
20
+ def self.invalid
21
+ @invalid ||= new(false, EMPTY_LOCATIONS, EMPTY_LOCATIONS)
22
+ end
23
+
24
+ def initialize(valid, evaluated_properties, evaluated_items)
25
+ @valid = valid
26
+ @evaluated_properties = evaluated_properties.freeze
27
+ @evaluated_items = evaluated_items.freeze
28
+ freeze
29
+ end
30
+
31
+ def valid?
32
+ @valid
33
+ end
34
+
35
+ def merge(other)
36
+ return self.class.invalid unless valid? && other.valid?
37
+ return self if other.evaluated_properties.empty? && other.evaluated_items.empty?
38
+ return other if evaluated_properties.empty? && evaluated_items.empty?
39
+
40
+ properties = if evaluated_properties.empty?
41
+ other.evaluated_properties
42
+ elsif other.evaluated_properties.empty?
43
+ evaluated_properties
44
+ else
45
+ evaluated_properties | other.evaluated_properties
46
+ end
47
+ items = if evaluated_items.empty?
48
+ other.evaluated_items
49
+ elsif other.evaluated_items.empty?
50
+ evaluated_items
51
+ else
52
+ evaluated_items | other.evaluated_items
53
+ end
54
+
55
+ self.class.valid(
56
+ evaluated_properties: properties,
57
+ evaluated_items: items
58
+ )
59
+ end
60
+ end
61
+
62
+ private_constant :Evaluation
63
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "limit_budget"
4
+
5
+ module Schemurai
6
+ module Internal
7
+ # Limit policy shared by evaluators; error storage and scalar state stay local.
8
+ module EvaluationLimits
9
+ def validate(instance, max_errors: nil, timeout: nil, budget: nil)
10
+ with_budget(max_errors, timeout, budget) { super(instance) }
11
+ end
12
+
13
+ def valid?(instance, max_errors: nil, timeout: nil, budget: nil)
14
+ with_budget(max_errors, timeout, budget) { super(instance) }
15
+ end
16
+
17
+ private def with_budget(max_errors, timeout, budget)
18
+ @budget = budget || LimitBudget.new(max_errors: max_errors, timeout: timeout)
19
+ result = yield
20
+ @budget.check_deadline!
21
+ result
22
+ ensure
23
+ @budget = nil
24
+ end
25
+
26
+ private def evaluate(node, instance)
27
+ @budget.checkpoint
28
+ super
29
+ end
30
+
31
+ private def evaluate_valid(node, instance)
32
+ @budget.checkpoint
33
+ super
34
+ end
35
+
36
+ private def valid_string?(node, value)
37
+ @budget.check_deadline!
38
+ result = super
39
+ @budget.check_deadline!
40
+ result
41
+ end
42
+
43
+ private def check_string(node, value)
44
+ @budget.check_deadline!
45
+ result = super
46
+ @budget.check_deadline!
47
+ result
48
+ end
49
+
50
+ private def valid_content?(schema, value)
51
+ @budget.check_deadline!
52
+ result = super
53
+ @budget.check_deadline!
54
+ result
55
+ end
56
+
57
+ private def check_content(schema, value)
58
+ @budget.check_deadline!
59
+ result = super
60
+ @budget.check_deadline!
61
+ result
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set" # rubocop:disable Lint/RedundantRequireStatement -- Required on Ruby 3.4.
4
+ require_relative "evaluation"
5
+
6
+ module Schemurai
7
+ module Internal
8
+ class EvaluationBuffer
9
+ attr_reader :evaluated_properties, :evaluated_items
10
+
11
+ def initialize
12
+ # Applicators can overlap. Array membership makes collecting and merging
13
+ # their annotations quadratic in the number of instance locations.
14
+ @evaluated_properties = Set.new
15
+ @evaluated_items = Set.new
16
+ end
17
+
18
+ def valid? = true
19
+
20
+ def reset
21
+ @evaluated_properties.clear
22
+ @evaluated_items.clear
23
+ self
24
+ end
25
+
26
+ def record_property(name)
27
+ @evaluated_properties.add(name)
28
+ self
29
+ end
30
+
31
+ def record_item(index)
32
+ @evaluated_items.add(index)
33
+ self
34
+ end
35
+
36
+ def merge(other)
37
+ return Evaluation.invalid unless other.valid?
38
+
39
+ properties = other.evaluated_properties
40
+ items = other.evaluated_items
41
+ @evaluated_properties.merge(properties) unless properties.empty?
42
+ @evaluated_items.merge(items) unless items.empty?
43
+ self
44
+ end
45
+ end
46
+
47
+ class EvaluationPool
48
+ def initialize
49
+ @buffers = []
50
+ @index = 0
51
+ end
52
+
53
+ def reset
54
+ @index = 0
55
+ end
56
+
57
+ def used? = !@buffers.empty?
58
+
59
+ def acquire
60
+ index = @index
61
+ @index += 1
62
+ (@buffers[index] ||= EvaluationBuffer.new).reset
63
+ end
64
+ end
65
+ end
66
+ end