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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1b157d6e772537cab2886ef6d89bd547992c7359b73a13a49b9d9cfcdcc9808
4
- data.tar.gz: f0e8e28536c58c95cf1848db949dad8007df0c7a6550dd8b0fd76439841617cd
3
+ metadata.gz: 01b0c89354b90232337525b7fe75c063053fecd2965036bb7ff600d2b4e823cb
4
+ data.tar.gz: f45549ac8cf41eae0c2b28220ca93fe87042cec2aa7c7926382a00f8f65c105d
5
5
  SHA512:
6
- metadata.gz: b6f8206124aab29ae13908951efdd011e276ca347a75c0abf79da9fb3a9988f91a0a59b0ec559b85a88f01d5975b9ea251f9f14bb7aba634b7c8f3625c288167
7
- data.tar.gz: 3fcd551c456cb81b53842c0736ae9e629676ca8c7c0e43017ee43991b00b995fde7a530f1c0cb38f81d5e7bb640b68a8ad8ad388934af73b06b4ef77c4e46a34
6
+ metadata.gz: 618d494971d7a1745075dae9b9e6155562f9c0f0fb058e923cd457f26bb062be2858db9f2ffaf711d035ee83490179e005869106f3ae29ef0c8011512bed2af9
7
+ data.tar.gz: 6ecd49a7320b6b936067808a2a0bd07d6e497418427d0e1612d624dd703a71c6eae99f9ae27ab0a94c2946c3a6495fdc2c11bf665b7d59a9fad628f285af0f5e
data/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/schemurai.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/schemurai)
4
4
  [![test / rspec](https://github.com/oakcask/schemurai/actions/workflows/test--rspec.yaml/badge.svg)](https://github.com/oakcask/schemurai/actions/workflows/test--rspec.yaml)
5
5
  [![lint / rubocop](https://github.com/oakcask/schemurai/actions/workflows/lint--rubocop.yaml/badge.svg)](https://github.com/oakcask/schemurai/actions/workflows/lint--rubocop.yaml)
6
+ [![lint / rbs](https://github.com/oakcask/schemurai/actions/workflows/lint--rbs.yaml/badge.svg)](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`. Ruby interpreter and YJIT baseline instructions are in
288
- [`benchmark/baselines/README.md`](benchmark/baselines/README.md).
290
+ `script/oracle-compare`.
289
291
 
290
292
  Run the linter with:
291
293
 
@@ -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: (evaluated_properties | other.evaluated_properties),
40
- evaluated_items: (evaluated_items | other.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
- invalid!(path, "uses a subclass of #{value.class.superclass}") unless supported_exact_scalar?(value)
31
+ invalid_at!(label, path, "uses a subclass of #{value.class.superclass}") unless supported_exact_scalar?(value)
32
32
  when Float
33
- invalid!(path, "must contain only finite numbers") unless value.finite?
34
- invalid!(path, "uses a Float subclass") unless value.instance_of?(Float)
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
- invalid!(path, "uses an Array subclass") unless value.instance_of?(Array)
37
- descend!(value, path, ancestors) do
38
- value.each_with_index { |item, index| validate_value!(item, pointer(path, index), ancestors) }
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
- invalid!(path, "uses a Hash subclass") unless value.instance_of?(Hash)
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
- invalid!(path, "contains a non-string object key") unless key.instance_of?(String)
48
+ invalid_at!(label, path, "contains a non-string object key") unless key.instance_of?(String)
45
49
 
46
- validate_value!(item, pointer(path, key), ancestors)
50
+ path << key
51
+ validate_value!(item, label, path, ancestors)
52
+ path.pop
47
53
  end
48
54
  end
49
55
  else
50
- invalid!(path, "contains unsupported #{value.class}")
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
- invalid!(path, "contains a recursive container") if ancestors.key?(value.object_id)
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 pointer(path, segment)
71
- escaped = segment.to_s.gsub("~", "~0").gsub("/", "~1")
72
- "#{path}/#{escaped}"
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 :pointer
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 = @compiled_roots.fetch(schema) { @compiled_roots[schema] = {} }
69
- cache_key = [base_uri.to_s, dialect]
70
- roots.fetch(cache_key) do
71
- roots[cache_key] = compile_atomically do |changes|
72
- root_dialect = dialect_for(schema, dialect, changes)
73
- compile_document(schema, base_uri.to_s, root_dialect, changes)
74
- end
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[:resources])
156
- changes[:resource_nodes].each do |resource, resource_nodes|
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[:uri_registry])
160
- nodes.concat(changes[:nodes])
190
+ uri_registry.update(changes.uri_registry)
191
+ nodes.concat(changes.nodes)
161
192
  if @nodes_by_document_location
162
- changes[:document_locations].each do |document_key, schema_path, node|
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[:dynamic_anchors].each do |resource, anchors|
197
+ changes.dynamic_anchors.each do |resource, anchors|
167
198
  (@dynamic_anchors[resource] ||= {}).update(anchors)
168
199
  end
169
- (@custom_dialects ||= {}).update(changes[:custom_dialects]) unless changes[:custom_dialects].empty?
170
- @dynamic_scope = true if changes[:requires_dynamic_scope]
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[:requires_dynamic_scope] = true
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[:nodes] << node
267
- changes[:document_locations] << [document_key, schema_path, node]
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[:uri_registry][base] = node
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[:resource_nodes][resource] ||= {})[node.resource_path] = node unless node.equal?(resource.root)
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[:resources][uri] = Resource.new(uri, root)
351
+ changes.mutable_resources[uri] = Resource.new(uri, root)
319
352
  end
320
353
 
321
354
  private def resource_at(uri, changes)
322
- changes[:resources].fetch(uri) { resources[uri] }
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[:uri_registry].key?(uri) || uri_registry.key?(uri)
359
+ return if changes.uri_registry.key?(uri) || uri_registry.key?(uri)
327
360
 
328
- changes[:uri_registry][uri] = node
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[:uri_registry]["#{resource.uri}##{name}"] = node
335
- ((changes[:dynamic_anchors][resource] ||= {})[name] = node) if dynamic
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[:custom_dialects] : (@custom_dialects ||= {})
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
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Schemurai
4
4
  # The installed Schemurai version.
5
- VERSION = "2.1.0"
5
+ VERSION = "2.2.0"
6
6
  end