schemurai 2.1.0 → 2.2.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 +4 -2
- data/lib/schemurai/evaluation.rb +19 -2
- data/lib/schemurai/schema_domain.rb +27 -19
- data/lib/schemurai/schema_graph.rb +70 -37
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/compiler.rb +166 -129
- data/lib/schemurai/vm/evaluator.rb +479 -190
- data/sig/schemurai.rbs +157 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 01b0c89354b90232337525b7fe75c063053fecd2965036bb7ff600d2b4e823cb
|
|
4
|
+
data.tar.gz: f45549ac8cf41eae0c2b28220ca93fe87042cec2aa7c7926382a00f8f65c105d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 618d494971d7a1745075dae9b9e6155562f9c0f0fb058e923cd457f26bb062be2858db9f2ffaf711d035ee83490179e005869106f3ae29ef0c8011512bed2af9
|
|
7
|
+
data.tar.gz: 6ecd49a7320b6b936067808a2a0bd07d6e497418427d0e1612d624dd703a71c6eae99f9ae27ab0a94c2946c3a6495fdc2c11bf665b7d59a9fad628f285af0f5e
|
data/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://badge.fury.io/rb/schemurai)
|
|
4
4
|
[](https://github.com/oakcask/schemurai/actions/workflows/test--rspec.yaml)
|
|
5
5
|
[](https://github.com/oakcask/schemurai/actions/workflows/lint--rubocop.yaml)
|
|
6
|
+
[](https://github.com/oakcask/schemurai/actions/workflows/lint--rbs.yaml)
|
|
6
7
|
|
|
7
8
|
A small, light-weight-dependency JSON Schema validator for Ruby supporting Draft 7,
|
|
8
9
|
Draft 2019-09, and Draft 2020-12. It covers the required cases in the official
|
|
@@ -11,6 +12,8 @@ precision, ECMA-262 regular expressions, content validation, anchors, and dynami
|
|
|
11
12
|
references. The dialect is selected from the schema's `$schema` URI; schemas that
|
|
12
13
|
omit `$schema` use Draft 7 for compatibility.
|
|
13
14
|
|
|
15
|
+
Schemurai ships RBS declarations for its public API in `sig/schemurai.rbs`.
|
|
16
|
+
|
|
14
17
|
```ruby
|
|
15
18
|
require "schemurai"
|
|
16
19
|
|
|
@@ -284,8 +287,7 @@ ruby script/oracle-cases --summary
|
|
|
284
287
|
```
|
|
285
288
|
|
|
286
289
|
The serialized oracle tools are `script/oracle-runner` and
|
|
287
|
-
`script/oracle-compare`.
|
|
288
|
-
[`benchmark/baselines/README.md`](benchmark/baselines/README.md).
|
|
290
|
+
`script/oracle-compare`.
|
|
289
291
|
|
|
290
292
|
Run the linter with:
|
|
291
293
|
|
data/lib/schemurai/evaluation.rb
CHANGED
|
@@ -34,10 +34,27 @@ module Schemurai
|
|
|
34
34
|
|
|
35
35
|
def merge(other)
|
|
36
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
|
|
37
54
|
|
|
38
55
|
self.class.valid(
|
|
39
|
-
evaluated_properties:
|
|
40
|
-
evaluated_items:
|
|
56
|
+
evaluated_properties: properties,
|
|
57
|
+
evaluated_items: items
|
|
41
58
|
)
|
|
42
59
|
end
|
|
43
60
|
end
|
|
@@ -8,7 +8,7 @@ module Schemurai
|
|
|
8
8
|
invalid!(label, "must be a boolean or an object")
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
validate_value!(schema, label, {})
|
|
11
|
+
validate_value!(schema, label, [], {})
|
|
12
12
|
schema
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -23,31 +23,37 @@ module Schemurai
|
|
|
23
23
|
schemas
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
module_function def validate_value!(value, path, ancestors)
|
|
26
|
+
module_function def validate_value!(value, label, path, ancestors)
|
|
27
27
|
case value
|
|
28
28
|
when nil, true, false
|
|
29
29
|
nil
|
|
30
30
|
when String, Integer
|
|
31
|
-
|
|
31
|
+
invalid_at!(label, path, "uses a subclass of #{value.class.superclass}") unless supported_exact_scalar?(value)
|
|
32
32
|
when Float
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
invalid_at!(label, path, "must contain only finite numbers") unless value.finite?
|
|
34
|
+
invalid_at!(label, path, "uses a Float subclass") unless value.instance_of?(Float)
|
|
35
35
|
when Array
|
|
36
|
-
|
|
37
|
-
descend!(value, path, ancestors) do
|
|
38
|
-
value.each_with_index
|
|
36
|
+
invalid_at!(label, path, "uses an Array subclass") unless value.instance_of?(Array)
|
|
37
|
+
descend!(value, label, path, ancestors) do
|
|
38
|
+
value.each_with_index do |item, index|
|
|
39
|
+
path << index
|
|
40
|
+
validate_value!(item, label, path, ancestors)
|
|
41
|
+
path.pop
|
|
42
|
+
end
|
|
39
43
|
end
|
|
40
44
|
when Hash
|
|
41
|
-
|
|
42
|
-
descend!(value, path, ancestors) do
|
|
45
|
+
invalid_at!(label, path, "uses a Hash subclass") unless value.instance_of?(Hash)
|
|
46
|
+
descend!(value, label, path, ancestors) do
|
|
43
47
|
value.each do |key, item|
|
|
44
|
-
|
|
48
|
+
invalid_at!(label, path, "contains a non-string object key") unless key.instance_of?(String)
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
path << key
|
|
51
|
+
validate_value!(item, label, path, ancestors)
|
|
52
|
+
path.pop
|
|
47
53
|
end
|
|
48
54
|
end
|
|
49
55
|
else
|
|
50
|
-
|
|
56
|
+
invalid_at!(label, path, "contains unsupported #{value.class}")
|
|
51
57
|
end
|
|
52
58
|
end
|
|
53
59
|
private_class_method :validate_value!
|
|
@@ -57,8 +63,8 @@ module Schemurai
|
|
|
57
63
|
end
|
|
58
64
|
private_class_method :supported_exact_scalar?
|
|
59
65
|
|
|
60
|
-
module_function def descend!(value, path, ancestors)
|
|
61
|
-
|
|
66
|
+
module_function def descend!(value, label, path, ancestors)
|
|
67
|
+
invalid_at!(label, path, "contains a recursive container") if ancestors.key?(value.object_id)
|
|
62
68
|
|
|
63
69
|
ancestors[value.object_id] = true
|
|
64
70
|
yield
|
|
@@ -67,11 +73,13 @@ module Schemurai
|
|
|
67
73
|
end
|
|
68
74
|
private_class_method :descend!
|
|
69
75
|
|
|
70
|
-
module_function def
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
module_function def invalid_at!(label, path, reason)
|
|
77
|
+
location = path.reduce(label.dup) do |result, segment|
|
|
78
|
+
result << "/" << segment.to_s.gsub("~", "~0").gsub("/", "~1")
|
|
79
|
+
end
|
|
80
|
+
invalid!(location, reason)
|
|
73
81
|
end
|
|
74
|
-
private_class_method :
|
|
82
|
+
private_class_method :invalid_at!
|
|
75
83
|
|
|
76
84
|
module_function def invalid!(path, reason)
|
|
77
85
|
raise Error, "invalid JSON-shaped #{path}: #{reason}"
|
|
@@ -13,6 +13,44 @@ require_relative "schema_domain"
|
|
|
13
13
|
module Schemurai
|
|
14
14
|
module Internal
|
|
15
15
|
class SchemaGraph
|
|
16
|
+
class CompilationChanges
|
|
17
|
+
EMPTY_HASH = {}.freeze
|
|
18
|
+
EMPTY_ARRAY = [].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :nodes
|
|
21
|
+
attr_accessor :requires_dynamic_scope
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@nodes = []
|
|
25
|
+
@requires_dynamic_scope = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resources = @resources || EMPTY_HASH
|
|
29
|
+
def mutable_resources = (@resources ||= {})
|
|
30
|
+
|
|
31
|
+
def resource_nodes = @resource_nodes || EMPTY_HASH
|
|
32
|
+
def mutable_resource_nodes = (@resource_nodes ||= {})
|
|
33
|
+
|
|
34
|
+
def uri_registry = @uri_registry || EMPTY_HASH
|
|
35
|
+
def mutable_uri_registry = (@uri_registry ||= {})
|
|
36
|
+
|
|
37
|
+
def dynamic_anchors = @dynamic_anchors || EMPTY_HASH
|
|
38
|
+
def mutable_dynamic_anchors = (@dynamic_anchors ||= {})
|
|
39
|
+
|
|
40
|
+
def custom_dialects = @custom_dialects || EMPTY_HASH
|
|
41
|
+
def mutable_custom_dialects = (@custom_dialects ||= {})
|
|
42
|
+
|
|
43
|
+
def document_locations
|
|
44
|
+
@document_locations || EMPTY_ARRAY
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def mutable_document_locations
|
|
48
|
+
@document_locations ||= []
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private_constant :CompilationChanges
|
|
53
|
+
|
|
16
54
|
class Resource
|
|
17
55
|
attr_reader :uri, :root
|
|
18
56
|
|
|
@@ -65,13 +103,15 @@ module Schemurai
|
|
|
65
103
|
|
|
66
104
|
def compile(schema, base_uri: nil, dialect: @default_dialect)
|
|
67
105
|
SchemaDomain.validate!(schema)
|
|
68
|
-
roots =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
106
|
+
roots = (@compiled_roots[schema] ||= {})
|
|
107
|
+
base_uri = base_uri.to_s
|
|
108
|
+
cache_key = [base_uri, dialect]
|
|
109
|
+
root = roots[cache_key]
|
|
110
|
+
return root if root
|
|
111
|
+
|
|
112
|
+
roots[cache_key] = compile_atomically do |changes|
|
|
113
|
+
root_dialect = dialect_for(schema, dialect, changes)
|
|
114
|
+
compile_document(schema, base_uri, root_dialect, changes)
|
|
75
115
|
end
|
|
76
116
|
end
|
|
77
117
|
|
|
@@ -132,16 +172,7 @@ module Schemurai
|
|
|
132
172
|
end
|
|
133
173
|
|
|
134
174
|
private def compilation_changes
|
|
135
|
-
|
|
136
|
-
resources: {},
|
|
137
|
-
resource_nodes: {},
|
|
138
|
-
uri_registry: {},
|
|
139
|
-
nodes: [],
|
|
140
|
-
document_locations: [],
|
|
141
|
-
dynamic_anchors: {},
|
|
142
|
-
custom_dialects: {},
|
|
143
|
-
requires_dynamic_scope: false
|
|
144
|
-
}
|
|
175
|
+
CompilationChanges.new
|
|
145
176
|
end
|
|
146
177
|
|
|
147
178
|
private def compile_atomically
|
|
@@ -152,22 +183,22 @@ module Schemurai
|
|
|
152
183
|
end
|
|
153
184
|
|
|
154
185
|
private def commit(changes)
|
|
155
|
-
resources.update(changes
|
|
156
|
-
changes
|
|
186
|
+
resources.update(changes.resources)
|
|
187
|
+
changes.resource_nodes.each do |resource, resource_nodes|
|
|
157
188
|
resource_nodes.each_value { |node| resource.add(node) }
|
|
158
189
|
end
|
|
159
|
-
uri_registry.update(changes
|
|
160
|
-
nodes.concat(changes
|
|
190
|
+
uri_registry.update(changes.uri_registry)
|
|
191
|
+
nodes.concat(changes.nodes)
|
|
161
192
|
if @nodes_by_document_location
|
|
162
|
-
changes
|
|
193
|
+
changes.document_locations.each do |document_key, schema_path, node|
|
|
163
194
|
(@nodes_by_document_location[document_key] ||= {})[schema_path] = node
|
|
164
195
|
end
|
|
165
196
|
end
|
|
166
|
-
changes
|
|
197
|
+
changes.dynamic_anchors.each do |resource, anchors|
|
|
167
198
|
(@dynamic_anchors[resource] ||= {}).update(anchors)
|
|
168
199
|
end
|
|
169
|
-
(@custom_dialects ||= {}).update(changes
|
|
170
|
-
@dynamic_scope = true if changes
|
|
200
|
+
(@custom_dialects ||= {}).update(changes.custom_dialects) unless changes.custom_dialects.empty?
|
|
201
|
+
@dynamic_scope = true if changes.requires_dynamic_scope
|
|
171
202
|
end
|
|
172
203
|
|
|
173
204
|
private def resolve_uncached(node, reference)
|
|
@@ -236,7 +267,7 @@ module Schemurai
|
|
|
236
267
|
private def compile_node(schema, inherited_base, dialect, schema_path, resource_path, resource, document_key, changes)
|
|
237
268
|
hash_schema = schema.is_a?(Hash)
|
|
238
269
|
if hash_schema && (schema.key?("$recursiveRef") || schema.key?("$dynamicRef"))
|
|
239
|
-
changes
|
|
270
|
+
changes.requires_dynamic_scope = true
|
|
240
271
|
end
|
|
241
272
|
dialect = dialect_for(schema, dialect, changes) if hash_schema && schema.key?("$schema")
|
|
242
273
|
if hash_schema && dialect.format_assertion? && schema.key?("format") &&
|
|
@@ -263,11 +294,13 @@ module Schemurai
|
|
|
263
294
|
resource_path: resource_path,
|
|
264
295
|
document_key: document_key
|
|
265
296
|
)
|
|
266
|
-
changes
|
|
267
|
-
|
|
297
|
+
changes.nodes << node
|
|
298
|
+
if @nodes_by_document_location
|
|
299
|
+
changes.mutable_document_locations << [document_key, schema_path, node]
|
|
300
|
+
end
|
|
268
301
|
|
|
269
302
|
if hash_schema && schema.key?("$id") && !exclusive_ref && !base.empty?
|
|
270
|
-
changes[
|
|
303
|
+
changes.mutable_uri_registry[base] = node
|
|
271
304
|
if starts_resource
|
|
272
305
|
resource = register_resource(strip_fragment(base), node, changes)
|
|
273
306
|
end
|
|
@@ -276,7 +309,7 @@ module Schemurai
|
|
|
276
309
|
resource ||= register_resource(strip_fragment(base), node, changes)
|
|
277
310
|
unless node.resource
|
|
278
311
|
node.resource = resource
|
|
279
|
-
(changes[
|
|
312
|
+
(changes.mutable_resource_nodes[resource] ||= {})[node.resource_path] = node unless node.equal?(resource.root)
|
|
280
313
|
end
|
|
281
314
|
|
|
282
315
|
if hash_schema && !exclusive_ref
|
|
@@ -315,24 +348,24 @@ module Schemurai
|
|
|
315
348
|
return resource if resource && resource.root.equal?(root)
|
|
316
349
|
return resource if resource
|
|
317
350
|
|
|
318
|
-
changes[
|
|
351
|
+
changes.mutable_resources[uri] = Resource.new(uri, root)
|
|
319
352
|
end
|
|
320
353
|
|
|
321
354
|
private def resource_at(uri, changes)
|
|
322
|
-
changes
|
|
355
|
+
changes.resources.fetch(uri) { resources[uri] }
|
|
323
356
|
end
|
|
324
357
|
|
|
325
358
|
private def register_uri_unless_present(uri, node, changes)
|
|
326
|
-
return if changes
|
|
359
|
+
return if changes.uri_registry.key?(uri) || uri_registry.key?(uri)
|
|
327
360
|
|
|
328
|
-
changes[
|
|
361
|
+
changes.mutable_uri_registry[uri] = node
|
|
329
362
|
end
|
|
330
363
|
|
|
331
364
|
private def register_anchor(node, resource, name, changes, dynamic: false)
|
|
332
365
|
return unless name.is_a?(String) && !name.empty?
|
|
333
366
|
|
|
334
|
-
changes[
|
|
335
|
-
((changes[
|
|
367
|
+
changes.mutable_uri_registry["#{resource.uri}##{name}"] = node
|
|
368
|
+
((changes.mutable_dynamic_anchors[resource] ||= {})[name] = node) if dynamic
|
|
336
369
|
end
|
|
337
370
|
|
|
338
371
|
private def node_by_document_location(document_key, schema_path)
|
|
@@ -392,7 +425,7 @@ module Schemurai
|
|
|
392
425
|
meta_schema = @external_schemas[uri] || @external_schemas[uri.delete_suffix("#")]
|
|
393
426
|
return unless meta_schema.is_a?(Hash) && meta_schema["$vocabulary"].is_a?(Hash)
|
|
394
427
|
|
|
395
|
-
custom_dialects = changes ? changes
|
|
428
|
+
custom_dialects = changes ? changes.mutable_custom_dialects : (@custom_dialects ||= {})
|
|
396
429
|
return custom_dialects[uri] if custom_dialects.key?(uri)
|
|
397
430
|
return @custom_dialects[uri] if @custom_dialects&.key?(uri)
|
|
398
431
|
|
data/lib/schemurai/version.rb
CHANGED