schemurai 3.0.0 → 3.0.2

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -4
  3. data/ext/schemurai_native/Cargo.lock +55 -1
  4. data/ext/schemurai_native/Cargo.toml +4 -1
  5. data/ext/schemurai_native/extconf.rb +1 -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 +74 -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 +1087 -307
  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 +126 -14
  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/integer_scan.rs +132 -0
  20. data/ext/schemurai_native/src/lib.rs +917 -238
  21. data/ext/schemurai_native/src/memo.rs +11 -38
  22. data/ext/schemurai_native/src/number/arbitrary.rs +154 -0
  23. data/ext/schemurai_native/src/number.rs +250 -59
  24. data/ext/schemurai_native/src/pattern.rs +147 -109
  25. data/ext/schemurai_native/src/pointer.rs +97 -0
  26. data/ext/schemurai_native/src/runtime/properties.rs +162 -0
  27. data/ext/schemurai_native/src/runtime/reports.rs +266 -0
  28. data/ext/schemurai_native/src/runtime/scopes.rs +430 -0
  29. data/ext/schemurai_native/src/runtime.rs +1020 -0
  30. data/ext/schemurai_native/src/scope.rs +60 -16
  31. data/ext/schemurai_native/src/string.rs +68 -0
  32. data/ext/schemurai_native/src/tracking.rs +244 -174
  33. data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
  34. data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
  35. data/lib/schemurai/container_checks.rb +534 -0
  36. data/lib/schemurai/decimal.rb +11 -0
  37. data/lib/schemurai/dialect.rb +0 -12
  38. data/lib/schemurai/ecma_regexp.rb +46 -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/native/diagnostics.rb +4 -3
  43. data/lib/schemurai/native/evaluator.rb +38 -10
  44. data/lib/schemurai/native/null_numeric.rb +23 -0
  45. data/lib/schemurai/number_checks.rb +128 -0
  46. data/lib/schemurai/string_checks.rb +166 -0
  47. data/lib/schemurai/type_checks.rb +59 -0
  48. data/lib/schemurai/value_equality.rb +72 -0
  49. data/lib/schemurai/version.rb +1 -1
  50. data/lib/schemurai/vm/compiler.rb +18 -26
  51. data/lib/schemurai/vm/evaluator.rb +87 -545
  52. data/lib/schemurai/vm/limited_evaluator.rb +7 -22
  53. metadata +25 -2
@@ -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
@@ -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
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "ipaddr"
6
+ require_relative "ecma_regexp"
7
+ require_relative "error_message"
8
+
9
+ module Schemurai
10
+ module Internal
11
+ module StringChecks
12
+ module Regexps
13
+ private def ecma_regexp(pattern)
14
+ regexps = regexp_cache
15
+ return regexps[pattern] if regexps.key?(pattern)
16
+
17
+ regexps[pattern] = EcmaRegexp.compile(pattern, @budget)
18
+ end
19
+
20
+ private def regexp_cache = (@regexps ||= {})
21
+ end
22
+
23
+ module TimedRegexps
24
+ private def regexp_budget = @budget
25
+
26
+ private def ecma_regexp(pattern)
27
+ budget = regexp_budget
28
+ budget.check_deadline!
29
+ result = super
30
+ budget.check_deadline!
31
+ result
32
+ end
33
+ end
34
+
35
+ module Evaluation
36
+ private def valid_string?(rules, value)
37
+ length = value.length
38
+ return false if rules.max_length && length > rules.max_length
39
+ return false if rules.min_length && length < rules.min_length
40
+ return false if rules.pattern && !ecma_regexp(rules.pattern).match?(value)
41
+ if rules.format && (@validate_format || rules.format_assertion)
42
+ return false unless rules.format.call(value)
43
+ end
44
+ if @validate_content && (rules.decode_base64 || rules.parse_json)
45
+ return valid_content?(rules, value)
46
+ end
47
+
48
+ true
49
+ rescue RegexpError, IPAddr::InvalidAddressError
50
+ false
51
+ end
52
+
53
+ private def valid_content?(rules, value)
54
+ decoded = rules.decode_base64 ? Base64.strict_decode64(value) : value
55
+ JSON.parse(decoded) if rules.parse_json
56
+ true
57
+ rescue ArgumentError, JSON::ParserError
58
+ false
59
+ end
60
+ end
61
+
62
+ class State
63
+ include Regexps
64
+
65
+ attr_reader :budget
66
+
67
+ def ecma_regexp(pattern)
68
+ @budget&.check_deadline!
69
+ result = super
70
+ @budget&.check_deadline!
71
+ result
72
+ end
73
+
74
+ def initialize(regexps, budget)
75
+ @regexps = regexps
76
+ @budget = budget
77
+ end
78
+ end
79
+
80
+ # Rust owns traversal and error limits; this frame retains arbitrary Ruby
81
+ # length results and the VM's rescue state between reached assertions.
82
+ class Frame
83
+ include Evaluation
84
+
85
+ def initialize(rules, value, state, flags)
86
+ @rules = rules
87
+ @value = value
88
+ @state = state
89
+ @budget = state.budget
90
+ @validate_format = (flags & 1) != 0
91
+ @validate_content = (flags & 2) != 0
92
+ @required = (flags & 64) != 0
93
+ @invalid_pattern = false
94
+ end
95
+
96
+ def string? = @value.is_a?(String)
97
+ def invalid_pattern? = @invalid_pattern
98
+
99
+ def valid?
100
+ return !@required unless string?
101
+
102
+ valid_string?(@rules, @value)
103
+ end
104
+
105
+ def prepare
106
+ @budget&.check_deadline!
107
+ check(2)
108
+ end
109
+
110
+ def check(step)
111
+ case step
112
+ when 2
113
+ @length = @value.length
114
+ true
115
+ when 3
116
+ exceeded = @length > @rules.max_length
117
+ exceeded ? false : true
118
+ when 4
119
+ below = @length < @rules.min_length
120
+ below ? false : true
121
+ when 5 then (!ecma_regexp(@rules.pattern).match?(@value)) ? false : true
122
+ when 6 then (!@rules.format.call(@value)) ? false : true
123
+ when 7 then valid_content?(@rules, @value)
124
+ else raise "unknown string assertion"
125
+ end
126
+ rescue RegexpError
127
+ @invalid_pattern = true
128
+ false
129
+ end
130
+
131
+ def message(keyword, rescued)
132
+ return ErrorMessage.invalid_pattern(@rules.pattern) if rescued
133
+
134
+ case keyword
135
+ when "type" then ErrorMessage.type("string", @value)
136
+ when "maxLength" then ErrorMessage.size(keyword, @rules.max_length, @length)
137
+ when "minLength" then ErrorMessage.size(keyword, @rules.min_length, @length)
138
+ when "pattern" then @invalid_pattern ? ErrorMessage.invalid_pattern(@rules.pattern) : ErrorMessage.pattern(@rules.pattern)
139
+ when "format" then ErrorMessage.format(@rules.format.name)
140
+ when "contentEncoding" then ErrorMessage.content_encoding
141
+ when "contentMediaType" then ErrorMessage.content_media_type
142
+ else raise "unknown string diagnostic"
143
+ end
144
+ end
145
+
146
+ private def valid_string?(rules, value)
147
+ @budget&.check_deadline!
148
+ result = super
149
+ @budget&.check_deadline!
150
+ result
151
+ end
152
+
153
+ private def valid_content?(rules, value)
154
+ @budget&.check_deadline!
155
+ result = super
156
+ @budget&.check_deadline!
157
+ result
158
+ end
159
+
160
+ private def ecma_regexp(pattern)
161
+ @state.ecma_regexp(pattern)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Internal
5
+ module TypeChecks
6
+ NULL = 1 << 0
7
+ BOOLEAN = 1 << 1
8
+ OBJECT = 1 << 2
9
+ ARRAY = 1 << 3
10
+ NUMBER = 1 << 4
11
+ INTEGER = 1 << 5
12
+ STRING = 1 << 6
13
+ BITS = {"null" => NULL, "boolean" => BOOLEAN, "object" => OBJECT, "array" => ARRAY,
14
+ "number" => NUMBER, "integer" => INTEGER, "string" => STRING}.freeze
15
+
16
+ module_function def number?(value)
17
+ value.is_a?(Numeric) && !value.is_a?(Complex)
18
+ end
19
+
20
+ module_function def integer?(value)
21
+ number?(value) && integral?(value)
22
+ end
23
+
24
+ module_function def integral?(value)
25
+ value.finite? && value.to_i == value
26
+ end
27
+
28
+ module_function def single?(value, mask)
29
+ case mask
30
+ when NULL then value.nil?
31
+ when BOOLEAN then value == true || value == false
32
+ when OBJECT then value.is_a?(Hash)
33
+ when ARRAY then value.is_a?(Array)
34
+ when NUMBER then number?(value)
35
+ when INTEGER then integer?(value)
36
+ when STRING then value.is_a?(String)
37
+ else false
38
+ end
39
+ end
40
+
41
+ # The VM's union opcode classifies in this order, even for a one-name union.
42
+ module_function def instance_mask(value, integer:)
43
+ return NULL if value.nil?
44
+ return BOOLEAN if value == true || value == false
45
+ return OBJECT if value.is_a?(Hash)
46
+ return ARRAY if value.is_a?(Array)
47
+ return STRING if value.is_a?(String)
48
+ return 0 unless number?(value)
49
+ return NUMBER unless integer
50
+
51
+ integral?(value) ? NUMBER | INTEGER : NUMBER
52
+ end
53
+
54
+ module_function def valid?(value, mask, union)
55
+ union ? (mask & instance_mask(value, integer: (mask & INTEGER) != 0)) != 0 : single?(value, mask)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "unique_items"
4
+
5
+ module Schemurai
6
+ module Internal
7
+ # Ruby method order for VM/native value comparisons. This traverses values,
8
+ # not schema instructions, and retains custom collection/equality methods.
9
+ module ValueEquality
10
+ private def json_equal?(left, right)
11
+ if left.is_a?(Numeric) && !left.is_a?(Complex)
12
+ return right.is_a?(Numeric) && !right.is_a?(Complex) && left == right
13
+ end
14
+ return false unless left.instance_of?(right.class)
15
+
16
+ case left
17
+ when Hash
18
+ left.length == right.length && left.all? do |key, value|
19
+ @budget&.checkpoint
20
+ right.key?(key) && json_equal?(value, right[key])
21
+ end
22
+ when Array
23
+ return false unless left.length == right.length
24
+
25
+ index = 0
26
+ while index < left.length
27
+ @budget&.checkpoint
28
+ return false unless json_equal?(left[index], right[index])
29
+ index += 1
30
+ end
31
+ true
32
+ else
33
+ left == right
34
+ end
35
+ end
36
+
37
+ module Checkpoints
38
+ private def json_equal?(left, right)
39
+ @budget.checkpoint
40
+ super
41
+ end
42
+
43
+ private def json_fingerprint(value)
44
+ @budget.checkpoint
45
+ super
46
+ end
47
+ end
48
+
49
+ class Runtime
50
+ include ValueEquality
51
+ include UniqueItems
52
+
53
+ def initialize(budget)
54
+ @budget = budget
55
+ end
56
+
57
+ def evaluate(operation, operand, instance)
58
+ case operation
59
+ when 0 then json_equal?(operand, instance)
60
+ when 1 then operand.any? { |candidate| json_equal?(candidate, instance) }
61
+ when 2 then unique_items?(instance)
62
+ else raise "unknown equality operation"
63
+ end
64
+ end
65
+ end
66
+
67
+ class LimitedRuntime < Runtime
68
+ include Checkpoints
69
+ end
70
+ end
71
+ end
72
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Schemurai
4
4
  # The installed Schemurai version.
5
- VERSION = "3.0.0"
5
+ VERSION = "3.0.2"
6
6
  end