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,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
|
data/lib/schemurai/version.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../decimal"
|
|
4
|
+
require_relative "../number_checks"
|
|
5
|
+
require_relative "../type_checks"
|
|
6
|
+
|
|
3
7
|
module Schemurai
|
|
4
8
|
module VM
|
|
5
9
|
NumberRules = Data.define(
|
|
@@ -11,21 +15,21 @@ module Schemurai
|
|
|
11
15
|
:multiple_of
|
|
12
16
|
)
|
|
13
17
|
|
|
14
|
-
MAXIMUM =
|
|
15
|
-
MINIMUM =
|
|
16
|
-
EXCLUSIVE_MAXIMUM =
|
|
17
|
-
EXCLUSIVE_MINIMUM =
|
|
18
|
-
MULTIPLE_OF =
|
|
18
|
+
MAXIMUM = Internal::NumberChecks::MAXIMUM
|
|
19
|
+
MINIMUM = Internal::NumberChecks::MINIMUM
|
|
20
|
+
EXCLUSIVE_MAXIMUM = Internal::NumberChecks::EXCLUSIVE_MAXIMUM
|
|
21
|
+
EXCLUSIVE_MINIMUM = Internal::NumberChecks::EXCLUSIVE_MINIMUM
|
|
22
|
+
MULTIPLE_OF = Internal::NumberChecks::MULTIPLE_OF
|
|
19
23
|
|
|
20
24
|
TypeRules = Data.define(:mask, :names)
|
|
21
25
|
|
|
22
|
-
TYPE_NULL =
|
|
23
|
-
TYPE_BOOLEAN =
|
|
24
|
-
TYPE_OBJECT =
|
|
25
|
-
TYPE_ARRAY =
|
|
26
|
-
TYPE_NUMBER =
|
|
27
|
-
TYPE_INTEGER =
|
|
28
|
-
TYPE_STRING =
|
|
26
|
+
TYPE_NULL = Internal::TypeChecks::NULL
|
|
27
|
+
TYPE_BOOLEAN = Internal::TypeChecks::BOOLEAN
|
|
28
|
+
TYPE_OBJECT = Internal::TypeChecks::OBJECT
|
|
29
|
+
TYPE_ARRAY = Internal::TypeChecks::ARRAY
|
|
30
|
+
TYPE_NUMBER = Internal::TypeChecks::NUMBER
|
|
31
|
+
TYPE_INTEGER = Internal::TypeChecks::INTEGER
|
|
32
|
+
TYPE_STRING = Internal::TypeChecks::STRING
|
|
29
33
|
|
|
30
34
|
TRACKS_EVALUATION = 1 << 0
|
|
31
35
|
TRACKS_DYNAMIC_SCOPE = 1 << 1
|
|
@@ -40,15 +44,7 @@ module Schemurai
|
|
|
40
44
|
KEYWORD_OBJECT = DIALECT::OBJECT
|
|
41
45
|
private_constant :DIALECT
|
|
42
46
|
|
|
43
|
-
TYPE_BITS =
|
|
44
|
-
"null" => TYPE_NULL,
|
|
45
|
-
"boolean" => TYPE_BOOLEAN,
|
|
46
|
-
"object" => TYPE_OBJECT,
|
|
47
|
-
"array" => TYPE_ARRAY,
|
|
48
|
-
"number" => TYPE_NUMBER,
|
|
49
|
-
"integer" => TYPE_INTEGER,
|
|
50
|
-
"string" => TYPE_STRING
|
|
51
|
-
}.freeze
|
|
47
|
+
TYPE_BITS = Internal::TypeChecks::BITS
|
|
52
48
|
|
|
53
49
|
TYPE_OPCODES = {
|
|
54
50
|
"null" => :type_null,
|
|
@@ -145,10 +141,6 @@ module Schemurai
|
|
|
145
141
|
freeze
|
|
146
142
|
end
|
|
147
143
|
|
|
148
|
-
def tracks_evaluation?
|
|
149
|
-
@tracks_evaluation
|
|
150
|
-
end
|
|
151
|
-
|
|
152
144
|
def recursive_anchor?
|
|
153
145
|
@recursive_anchor
|
|
154
146
|
end
|
|
@@ -411,7 +403,7 @@ module Schemurai
|
|
|
411
403
|
private def compile_decimal(value)
|
|
412
404
|
return value if value.nil? || value.is_a?(Integer) || value.is_a?(Rational)
|
|
413
405
|
|
|
414
|
-
|
|
406
|
+
Internal::Decimal.parse(value)
|
|
415
407
|
end
|
|
416
408
|
end
|
|
417
409
|
end
|