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.
- checksums.yaml +4 -4
- data/README.md +28 -7
- data/ext/schemurai_native/Cargo.lock +59 -1
- data/ext/schemurai_native/Cargo.toml +4 -1
- data/ext/schemurai_native/extconf.rb +5 -1
- data/ext/schemurai_native/glue.c +1679 -62
- data/ext/schemurai_native/protocol.def +45 -0
- data/ext/schemurai_native/src/budget.rs +49 -0
- data/ext/schemurai_native/src/container.rs +460 -0
- data/ext/schemurai_native/src/content.rs +56 -27
- data/ext/schemurai_native/src/diagnostics/array.rs +392 -0
- data/ext/schemurai_native/src/diagnostics/object.rs +218 -0
- data/ext/schemurai_native/src/diagnostics.rs +1064 -325
- data/ext/schemurai_native/src/enumeration.rs +6 -1
- data/ext/schemurai_native/src/evaluation.rs +49 -20
- data/ext/schemurai_native/src/format.rs +8 -6
- data/ext/schemurai_native/src/graph.rs +37 -0
- data/ext/schemurai_native/src/instance_graph.rs +90 -0
- data/ext/schemurai_native/src/lib.rs +910 -237
- data/ext/schemurai_native/src/memo.rs +11 -38
- data/ext/schemurai_native/src/number/arbitrary.rs +210 -0
- data/ext/schemurai_native/src/number.rs +250 -59
- data/ext/schemurai_native/src/pattern.rs +113 -101
- data/ext/schemurai_native/src/pointer.rs +97 -0
- data/ext/schemurai_native/src/runtime/properties.rs +162 -0
- data/ext/schemurai_native/src/runtime/reports.rs +266 -0
- data/ext/schemurai_native/src/runtime/scopes.rs +430 -0
- data/ext/schemurai_native/src/runtime.rs +1020 -0
- data/ext/schemurai_native/src/scope.rs +60 -16
- data/ext/schemurai_native/src/string.rs +68 -0
- data/ext/schemurai_native/src/tracking.rs +244 -174
- data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
- data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
- data/lib/schemurai/container_checks.rb +534 -0
- data/lib/schemurai/decimal.rb +11 -0
- data/lib/schemurai/dialect.rb +0 -12
- data/lib/schemurai/ecma_regexp.rb +46 -0
- data/lib/schemurai/evaluation_limits.rb +65 -0
- data/lib/schemurai/evaluation_pool.rb +66 -0
- data/lib/schemurai/evaluator.rb +9 -48
- data/lib/schemurai/json_pointer.rb +11 -0
- data/lib/schemurai/limited_evaluator.rb +2 -59
- data/lib/schemurai/native/diagnostics.rb +4 -3
- data/lib/schemurai/native/evaluator.rb +38 -10
- data/lib/schemurai/native/null_numeric.rb +23 -0
- data/lib/schemurai/number_checks.rb +128 -0
- data/lib/schemurai/string_checks.rb +166 -0
- data/lib/schemurai/type_checks.rb +59 -0
- data/lib/schemurai/value_equality.rb +72 -0
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/compiler.rb +18 -26
- data/lib/schemurai/vm/evaluator.rb +87 -545
- data/lib/schemurai/vm/limited_evaluator.rb +8 -78
- 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
|
data/lib/schemurai/evaluator.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 << "/" <<
|
|
855
|
+
pointer << "/" << Internal::JsonPointer.escape(segment)
|
|
895
856
|
end
|
|
896
857
|
|
|
897
858
|
private_constant :MISSING_SEGMENT
|
|
@@ -1,63 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "limit_exceeded"
|
|
4
|
-
require_relative "
|
|
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
|
-
|
|
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 "../
|
|
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
|
-
@
|
|
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
|
|
48
|
+
return result
|
|
29
49
|
end
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
53
|
-
|
|
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
|