schemurai 3.0.1 → 3.1.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -7
  3. data/ext/schemurai_native/Cargo.lock +59 -1
  4. data/ext/schemurai_native/Cargo.toml +4 -1
  5. data/ext/schemurai_native/extconf.rb +5 -1
  6. data/ext/schemurai_native/glue.c +1679 -62
  7. data/ext/schemurai_native/protocol.def +45 -0
  8. data/ext/schemurai_native/src/budget.rs +49 -0
  9. data/ext/schemurai_native/src/container.rs +460 -0
  10. data/ext/schemurai_native/src/content.rs +56 -27
  11. data/ext/schemurai_native/src/diagnostics/array.rs +392 -0
  12. data/ext/schemurai_native/src/diagnostics/object.rs +218 -0
  13. data/ext/schemurai_native/src/diagnostics.rs +1064 -325
  14. data/ext/schemurai_native/src/enumeration.rs +6 -1
  15. data/ext/schemurai_native/src/evaluation.rs +49 -20
  16. data/ext/schemurai_native/src/format.rs +8 -6
  17. data/ext/schemurai_native/src/graph.rs +37 -0
  18. data/ext/schemurai_native/src/instance_graph.rs +90 -0
  19. data/ext/schemurai_native/src/lib.rs +910 -237
  20. data/ext/schemurai_native/src/memo.rs +11 -38
  21. data/ext/schemurai_native/src/number/arbitrary.rs +210 -0
  22. data/ext/schemurai_native/src/number.rs +250 -59
  23. data/ext/schemurai_native/src/pattern.rs +113 -101
  24. data/ext/schemurai_native/src/pointer.rs +97 -0
  25. data/ext/schemurai_native/src/runtime/properties.rs +162 -0
  26. data/ext/schemurai_native/src/runtime/reports.rs +266 -0
  27. data/ext/schemurai_native/src/runtime/scopes.rs +430 -0
  28. data/ext/schemurai_native/src/runtime.rs +1020 -0
  29. data/ext/schemurai_native/src/scope.rs +60 -16
  30. data/ext/schemurai_native/src/string.rs +68 -0
  31. data/ext/schemurai_native/src/tracking.rs +244 -174
  32. data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
  33. data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
  34. data/lib/schemurai/container_checks.rb +534 -0
  35. data/lib/schemurai/decimal.rb +11 -0
  36. data/lib/schemurai/dialect.rb +0 -12
  37. data/lib/schemurai/ecma_regexp.rb +46 -0
  38. data/lib/schemurai/evaluation_limits.rb +65 -0
  39. data/lib/schemurai/evaluation_pool.rb +66 -0
  40. data/lib/schemurai/evaluator.rb +9 -48
  41. data/lib/schemurai/json_pointer.rb +11 -0
  42. data/lib/schemurai/limited_evaluator.rb +2 -59
  43. data/lib/schemurai/native/diagnostics.rb +4 -3
  44. data/lib/schemurai/native/evaluator.rb +38 -10
  45. data/lib/schemurai/native/null_numeric.rb +23 -0
  46. data/lib/schemurai/number_checks.rb +128 -0
  47. data/lib/schemurai/string_checks.rb +166 -0
  48. data/lib/schemurai/type_checks.rb +59 -0
  49. data/lib/schemurai/value_equality.rb +72 -0
  50. data/lib/schemurai/version.rb +1 -1
  51. data/lib/schemurai/vm/compiler.rb +18 -26
  52. data/lib/schemurai/vm/evaluator.rb +87 -545
  53. data/lib/schemurai/vm/limited_evaluator.rb +8 -78
  54. metadata +25 -2
@@ -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
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "decimal"
4
+ require_relative "type_checks"
3
5
  require "json"
4
6
  require "base64"
5
7
  require_relative "evaluation"
6
8
  require_relative "error_message"
7
9
  require_relative "unique_items"
10
+ require_relative "ecma_regexp"
11
+ require_relative "json_pointer"
8
12
 
9
13
  module Schemurai
10
14
  module Internal
@@ -747,20 +751,11 @@ module Schemurai
747
751
  end
748
752
 
749
753
  private def type?(value, type)
750
- case type
751
- when "null" then value.nil?
752
- when "boolean" then value == true || value == false
753
- when "object" then value.is_a?(Hash)
754
- when "array" then value.is_a?(Array)
755
- when "number" then number?(value)
756
- when "integer" then number?(value) && value.finite? && value.to_i == value
757
- when "string" then value.is_a?(String)
758
- else false
759
- end
754
+ TypeChecks.single?(value, TypeChecks::BITS.fetch(type, 0))
760
755
  end
761
756
 
762
757
  private def number?(value)
763
- value.is_a?(Numeric) && !value.is_a?(Complex)
758
+ TypeChecks.number?(value)
764
759
  end
765
760
 
766
761
  private def json_equal?(left, right)
@@ -784,7 +779,7 @@ module Schemurai
784
779
  private def decimal(value)
785
780
  return value if value.is_a?(Integer) || value.is_a?(Rational)
786
781
 
787
- Rational(value.to_s)
782
+ Internal::Decimal.parse(value)
788
783
  end
789
784
 
790
785
  # Ruby and ECMA-262 differ in their ASCII character classes, anchors, and
@@ -793,41 +788,7 @@ module Schemurai
793
788
  regexps = (@regexps ||= {})
794
789
  return regexps[pattern] if regexps.key?(pattern)
795
790
 
796
- whitespace = "\\u0009-\\u000D\\u0020\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000\\uFEFF"
797
- translated = +""
798
- escaped = false
799
- in_class = false
800
- pattern.each_char do |character|
801
- @budget&.checkpoint
802
- if escaped
803
- translated << case character
804
- when "d" then in_class ? "0-9" : "[0-9]"
805
- when "D" then in_class ? "^0-9" : "[^0-9]"
806
- when "w" then in_class ? "A-Za-z0-9_" : "[A-Za-z0-9_]"
807
- when "W" then in_class ? "^A-Za-z0-9_" : "[^A-Za-z0-9_]"
808
- when "s" then in_class ? whitespace : "[#{whitespace}]"
809
- when "S" then in_class ? "^#{whitespace}" : "[^#{whitespace}]"
810
- else "\\#{character}"
811
- end
812
- escaped = false
813
- elsif character == "\\"
814
- escaped = true
815
- elsif character == "["
816
- in_class = true
817
- translated << character
818
- elsif character == "]"
819
- in_class = false
820
- translated << character
821
- elsif character == "^" && !in_class
822
- translated << "\\A"
823
- elsif character == "$" && !in_class
824
- translated << "\\z"
825
- else
826
- translated << character
827
- end
828
- end
829
- translated << "\\" if escaped
830
- regexps[pattern] = Regexp.new(translated)
791
+ regexps[pattern] = Internal::EcmaRegexp.compile(pattern, @budget)
831
792
  end
832
793
 
833
794
  private def check_content(schema, value)
@@ -891,7 +852,7 @@ module Schemurai
891
852
  end
892
853
 
893
854
  private def append_pointer_segment(pointer, segment)
894
- pointer << "/" << segment.to_s.gsub("~", "~0").gsub("/", "~1")
855
+ pointer << "/" << Internal::JsonPointer.escape(segment)
895
856
  end
896
857
 
897
858
  private_constant :MISSING_SEGMENT
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Internal
5
+ module JsonPointer
6
+ module_function def escape(segment)
7
+ segment.to_s.gsub("~", "~0").gsub("/", "~1")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,63 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "limit_exceeded"
4
- require_relative "limit_budget"
4
+ require_relative "evaluation_limits"
5
5
  require_relative "evaluator"
6
6
 
7
7
  module Schemurai
8
8
  module Internal
9
9
  # Ruby evaluation with invocation-local limits.
10
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
11
+ include EvaluationLimits
41
12
 
42
13
  private def add_error(keyword, message = nil, append_keyword: true, &block)
43
14
  @budget.reserve_error if @error_callback
44
15
  super
45
16
  end
46
17
 
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
18
  private def decimal(value)
62
19
  @budget.check_deadline!
63
20
  result = super
@@ -72,20 +29,6 @@ module Schemurai
72
29
  result
73
30
  end
74
31
 
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
32
  private def json_equal?(left, right)
90
33
  @budget.checkpoint
91
34
  super
@@ -15,9 +15,9 @@ module Schemurai
15
15
  private_constant :TYPE_NAMES, :ATTRIBUTES
16
16
 
17
17
  module_function def error(row)
18
- keyword, instance_path, schema_path, opcode, operand, instance, detail = row
18
+ keyword, instance_path, schema_path, opcode, operand, instance, detail, rendered_message = row
19
19
  ValidationError.new(keyword: keyword, instance_path: instance_path, schema_path: schema_path,
20
- message: message(keyword, opcode, operand, instance, detail))
20
+ message: rendered_message || message(keyword, opcode, operand, instance, detail))
21
21
  end
22
22
 
23
23
  module_function def message(keyword, opcode, operand, instance, detail) # rubocop:disable Metrics/MethodLength
@@ -32,7 +32,7 @@ module Schemurai
32
32
  when "multipleOf" then messages.multiple_of(operand.multiple_of)
33
33
  when "maxLength", "minLength", "maxItems", "minItems", "maxProperties", "minProperties"
34
34
  messages.size(keyword, operand.public_send(ATTRIBUTES.fetch(keyword)), detail)
35
- when "pattern" then messages.pattern(operand.pattern)
35
+ when "pattern" then (detail == 1) ? messages.invalid_pattern(operand.pattern) : messages.pattern(operand.pattern)
36
36
  when "format" then messages.format(operand.format.name)
37
37
  when "contentEncoding" then messages.content_encoding
38
38
  when "contentMediaType" then messages.content_media_type
@@ -43,6 +43,7 @@ module Schemurai
43
43
  when "anyOf" then messages.any_of
44
44
  when "oneOf" then messages.one_of(detail)
45
45
  when "not" then messages.not
46
+ when "$ref", "$recursiveRef", "$dynamicRef" then operand
46
47
  else raise "unknown native diagnostic #{keyword.inspect}"
47
48
  end
48
49
  end
@@ -1,8 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../vm/limited_evaluator"
3
+ require_relative "../decimal"
4
+ require_relative "../number_checks"
5
+ require_relative "../string_checks"
6
+ require_relative "../value_equality"
7
+ require_relative "../container_checks"
8
+ require_relative "../type_checks"
9
+ require_relative "../vm/compiler"
10
+ require_relative "../evaluation_pool"
11
+ require_relative "../limit_budget"
12
+ require_relative "../ecma_regexp"
13
+ require_relative "../json_pointer"
4
14
  require "schemurai_native"
5
15
  require_relative "diagnostics"
16
+ require_relative "null_numeric"
6
17
 
7
18
  module Schemurai
8
19
  module Native
@@ -15,29 +26,37 @@ module Schemurai
15
26
  @root = root
16
27
  @content = content
17
28
  @format = format
29
+ @graph_size = @graph.nodes.length
18
30
  @compiled = compiled || Program.new(root, compiler, format, content)
31
+ @number_state = Internal::NumberChecks::State.new
32
+ @regexps = {}
33
+ @scope_state = []
34
+ @report_state = [nil, Internal::EvaluationPool.new]
35
+ @reference_state = []
36
+ @active_state = {}.compare_by_identity
19
37
  @program = @compiled.runner(root)
20
- @vm = VM::Evaluator.new(graph, compiler, root, content: content, format: format)
38
+ @program.share_scalar_state(@number_state, @regexps, @scope_state, @reference_state, @active_state, @report_state)
39
+ @unresolved = @compiled.unresolved_references?
21
40
  end
22
41
 
23
42
  def valid?(instance, max_errors: nil, timeout: nil)
43
+ refresh_unresolved
24
44
  if !max_errors.nil? || !timeout.nil?
25
45
  budget = Internal::LimitBudget.new(max_errors: max_errors, timeout: timeout)
26
46
  result = @program.valid_with_budget(instance, budget)
27
47
  budget.check_deadline!
28
- return result.nil? ? limited_vm.valid?(instance, budget: budget) : result
48
+ return result
29
49
  end
30
- result = @program.valid?(instance)
31
- result.nil? ? @vm.valid?(instance) : result
50
+ @program.valid?(instance)
32
51
  end
33
52
 
34
53
  def validate(instance, max_errors: nil, timeout: nil)
54
+ refresh_unresolved
35
55
  if !max_errors.nil? || !timeout.nil?
36
56
  budget = Internal::LimitBudget.new(max_errors: max_errors, timeout: timeout)
37
57
  rows = @program.diagnostics_with_budget(instance, budget)
38
- return limited_vm.validate(instance, budget: budget) if rows.nil?
39
58
 
40
- errors = rows.map do |row|
59
+ errors = (rows == false) ? nil : rows.map do |row|
41
60
  budget.reserve_error
42
61
  Diagnostics.error(row)
43
62
  end
@@ -45,12 +64,21 @@ module Schemurai
45
64
  return Result.new(errors)
46
65
  end
47
66
  rows = @program.diagnostics(instance)
48
- return @vm.validate(instance) if rows.nil?
67
+
68
+ return Result.new(nil) if rows == false
49
69
 
50
70
  Result.new(rows.empty? ? rows : rows.map { |row| Diagnostics.error(row) })
51
71
  end
52
- private def limited_vm
53
- VM::LimitedEvaluator.new(@graph, @compiler, @root, content: @content, format: @format)
72
+ private def refresh_unresolved
73
+ while @unresolved && @graph_size != @graph.nodes.length
74
+ graph_size = @graph.nodes.length
75
+ compiled = Program.new(@root, @compiler, @format, @content)
76
+ @program = compiled.runner(@root)
77
+ @program.share_scalar_state(@number_state, @regexps, @scope_state, @reference_state, @active_state, @report_state)
78
+ @compiled = compiled
79
+ @unresolved = @compiled.unresolved_references?
80
+ @graph_size = graph_size
81
+ end
54
82
  end
55
83
  end
56
84
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Native
5
+ # Native traversal selects one reached assertion and its comparison mode.
6
+ # Ruby dispatch preserves the exception and method visibility for nil.
7
+ module NullNumeric
8
+ module_function def valid?(value, bit, decimal)
9
+ actual = if decimal && !value.is_a?(Integer) && !value.is_a?(Rational)
10
+ Internal::Decimal.parse(value)
11
+ else
12
+ value
13
+ end
14
+ case bit
15
+ when Protocol::MAXIMUM, Protocol::MINIMUM, Protocol::EXCLUSIVE_MAXIMUM, Protocol::EXCLUSIVE_MINIMUM
16
+ Internal::NumberChecks.bound?(actual, bit, nil)
17
+ when Protocol::MULTIPLE_OF then nil.positive? && actual.remainder(nil).zero?
18
+ else raise "unknown null numeric assertion"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "decimal"
4
+ require_relative "type_checks"
5
+
6
+ module Schemurai
7
+ module Internal
8
+ module NumberChecks
9
+ MAXIMUM = 1 << 0
10
+ MINIMUM = 1 << 1
11
+ EXCLUSIVE_MAXIMUM = 1 << 2
12
+ EXCLUSIVE_MINIMUM = 1 << 3
13
+ MULTIPLE_OF = 1 << 4
14
+
15
+ class State
16
+ CACHE_LIMIT = 16
17
+ private_constant :CACHE_LIMIT
18
+
19
+ attr_writer :budget
20
+
21
+ def decimal(value)
22
+ @budget&.check_deadline!
23
+ result = cached_decimal(value)
24
+ @budget&.check_deadline!
25
+ result
26
+ end
27
+
28
+ private def cached_decimal(value)
29
+ return value if value.is_a?(Integer) || value.is_a?(Rational)
30
+
31
+ decimals = (@decimals ||= {})
32
+ return decimals[value] if decimals.key?(value)
33
+
34
+ decimals.clear if decimals.length >= CACHE_LIMIT
35
+ decimals[value] = Decimal.parse(value)
36
+ end
37
+
38
+ def multiple?(rules, value)
39
+ results = (@multiple_results ||= {}.compare_by_identity)
40
+ values = (results[rules] ||= {})
41
+ return values[value] if values.key?(value)
42
+
43
+ values.clear if values.length >= CACHE_LIMIT
44
+ divisor = rules.multiple_of
45
+ values[value] = divisor.positive? && decimal(value).remainder(divisor).zero?
46
+ end
47
+ end
48
+
49
+ module_function def actual(rules, value, state)
50
+ (rules.mask & MULTIPLE_OF).zero? ? value : state.decimal(value)
51
+ end
52
+
53
+ module_function def bound?(actual, bit, limit)
54
+ violated = case bit
55
+ when MAXIMUM then actual > limit
56
+ when MINIMUM then actual < limit
57
+ when EXCLUSIVE_MAXIMUM then actual >= limit
58
+ when EXCLUSIVE_MINIMUM then actual <= limit
59
+ else raise "unknown numeric bound"
60
+ end
61
+ violated ? false : true
62
+ end
63
+
64
+ # Both the VM and native validity frames use the same ordered rules.
65
+ module Evaluation
66
+ private def number_state
67
+ @number_state ||= State.new
68
+ end
69
+
70
+ private def valid_number?(rules, value)
71
+ mask = rules.mask
72
+ actual = (mask & MULTIPLE_OF).zero? ? value : number_state.decimal(value)
73
+ return false if (mask & MAXIMUM) != 0 && actual > rules.maximum
74
+ return false if (mask & MINIMUM) != 0 && actual < rules.minimum
75
+ return false if (mask & EXCLUSIVE_MAXIMUM) != 0 && actual >= rules.exclusive_maximum
76
+ return false if (mask & EXCLUSIVE_MINIMUM) != 0 && actual <= rules.exclusive_minimum
77
+ return true if (mask & MULTIPLE_OF).zero?
78
+
79
+ valid_multiple?(rules, value)
80
+ end
81
+
82
+ private def valid_multiple?(rules, value)
83
+ number_state.multiple?(rules, value)
84
+ end
85
+ end
86
+
87
+ # One native numeric instruction owns this frame while Rust yields between
88
+ # reached assertions. No instance method runs during construction.
89
+ class Frame
90
+ include Evaluation
91
+
92
+ def initialize(rules, value, state)
93
+ @rules = rules
94
+ @value = value
95
+ @number_state = state
96
+ end
97
+
98
+ def numeric? = TypeChecks.number?(@value)
99
+ def integral? = TypeChecks.integral?(@value)
100
+
101
+ def valid?(required)
102
+ return required.zero? unless numeric?
103
+ if required == TypeChecks::INTEGER
104
+ return false unless integral?
105
+ end
106
+
107
+ valid_number?(@rules, @value)
108
+ end
109
+
110
+ def prepare
111
+ @actual = NumberChecks.actual(@rules, @value, @number_state)
112
+ true
113
+ end
114
+
115
+ def check(bit)
116
+ case bit
117
+ when MAXIMUM then NumberChecks.bound?(@actual, bit, @rules.maximum)
118
+ when MINIMUM then NumberChecks.bound?(@actual, bit, @rules.minimum)
119
+ when EXCLUSIVE_MAXIMUM then NumberChecks.bound?(@actual, bit, @rules.exclusive_maximum)
120
+ when EXCLUSIVE_MINIMUM then NumberChecks.bound?(@actual, bit, @rules.exclusive_minimum)
121
+ when MULTIPLE_OF then @number_state.multiple?(@rules, @value)
122
+ else raise "unknown numeric assertion"
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end