schemurai 3.1.0-aarch64-linux-gnu

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +395 -0
  4. data/lib/schemurai/backend.rb +34 -0
  5. data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
  6. data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
  7. data/lib/schemurai/container_checks.rb +534 -0
  8. data/lib/schemurai/decimal.rb +11 -0
  9. data/lib/schemurai/dialect.rb +106 -0
  10. data/lib/schemurai/dialect_keywords.rb +91 -0
  11. data/lib/schemurai/dialects/draft2019_09.rb +27 -0
  12. data/lib/schemurai/dialects/draft2020_12.rb +27 -0
  13. data/lib/schemurai/dialects/draft7.rb +27 -0
  14. data/lib/schemurai/ecma_regexp.rb +46 -0
  15. data/lib/schemurai/error_message.rb +102 -0
  16. data/lib/schemurai/evaluation.rb +63 -0
  17. data/lib/schemurai/evaluation_limits.rb +65 -0
  18. data/lib/schemurai/evaluation_pool.rb +66 -0
  19. data/lib/schemurai/evaluator.rb +861 -0
  20. data/lib/schemurai/formats.rb +403 -0
  21. data/lib/schemurai/json_pointer.rb +11 -0
  22. data/lib/schemurai/limit_budget.rb +78 -0
  23. data/lib/schemurai/limit_exceeded.rb +18 -0
  24. data/lib/schemurai/limited_evaluator.rb +43 -0
  25. data/lib/schemurai/meta_schemas/draft2019_09.rb +149 -0
  26. data/lib/schemurai/meta_schemas/draft2020_12.rb +149 -0
  27. data/lib/schemurai/meta_schemas/draft7.rb +121 -0
  28. data/lib/schemurai/meta_schemas.rb +28 -0
  29. data/lib/schemurai/native/4.0/schemurai_native.so +0 -0
  30. data/lib/schemurai/native/diagnostics.rb +53 -0
  31. data/lib/schemurai/native/evaluator.rb +87 -0
  32. data/lib/schemurai/native/null_numeric.rb +23 -0
  33. data/lib/schemurai/number_checks.rb +128 -0
  34. data/lib/schemurai/schema_domain.rb +95 -0
  35. data/lib/schemurai/schema_graph.rb +538 -0
  36. data/lib/schemurai/schema_node.rb +55 -0
  37. data/lib/schemurai/string_checks.rb +166 -0
  38. data/lib/schemurai/type_checks.rb +59 -0
  39. data/lib/schemurai/unique_items.rb +68 -0
  40. data/lib/schemurai/value_equality.rb +72 -0
  41. data/lib/schemurai/version.rb +6 -0
  42. data/lib/schemurai/vm/compiler.rb +410 -0
  43. data/lib/schemurai/vm/evaluator.rb +663 -0
  44. data/lib/schemurai/vm/limited_evaluator.rb +27 -0
  45. data/lib/schemurai.rb +307 -0
  46. data/lib/schemurai_native.rb +4 -0
  47. data/sig/schemurai.rbs +108 -0
  48. metadata +121 -0
@@ -0,0 +1,538 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require_relative "dialects/draft7"
5
+ require_relative "dialects/draft2019_09"
6
+ require_relative "dialects/draft2020_12"
7
+ require_relative "meta_schemas/draft7"
8
+ require_relative "meta_schemas/draft2019_09"
9
+ require_relative "meta_schemas/draft2020_12"
10
+ require_relative "schema_node"
11
+ require_relative "schema_domain"
12
+
13
+ module Schemurai
14
+ module Internal
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
+
54
+ class Resource
55
+ attr_reader :uri, :root
56
+ attr_accessor :dynamic_anchor_count
57
+
58
+ def initialize(uri, root)
59
+ @dynamic_anchor_count = 0
60
+ @uri = uri
61
+ @root = root
62
+ @nodes = nil
63
+ end
64
+
65
+ def add(node)
66
+ (@nodes ||= {})[node.resource_path] = node unless node.equal?(root)
67
+ node.resource = self unless node.resource.equal?(self)
68
+ end
69
+
70
+ def node_at(pointer)
71
+ return root if pointer.empty?
72
+
73
+ @nodes&.[](pointer)
74
+ end
75
+
76
+ def nodes
77
+ @nodes || EMPTY_NODES
78
+ end
79
+
80
+ EMPTY_NODES = {}.freeze
81
+ private_constant :EMPTY_NODES
82
+ end
83
+
84
+ attr_reader :resources, :uri_registry, :nodes
85
+
86
+ def initialize(schemas: {}, dialect: Dialect.resolve)
87
+ SchemaDomain.validate_registry!(schemas)
88
+ @external_schemas = schemas.dup
89
+ @indexed_external_schemas = nil
90
+ @compiled_roots = {}.compare_by_identity
91
+ @meta_schema_roots = {}
92
+ @resources = {}
93
+ @uri_registry = {}
94
+ @nodes = []
95
+ @nodes_by_document_location = nil
96
+ @resolved_refs = nil
97
+ @dynamic_anchors = {}
98
+ # External schemas are indexed lazily, so their references are not yet
99
+ # available to compile_node. Track conservatively from the caller.
100
+ # Once required, dynamic scope tracking must remain enabled because all
101
+ # evaluators sharing this graph may reach the dynamic reference.
102
+ @dynamic_scope = !schemas.empty?
103
+ @default_dialect = dialect
104
+ end
105
+
106
+ def compile(schema, base_uri: nil, dialect: @default_dialect)
107
+ SchemaDomain.validate!(schema)
108
+ roots = (@compiled_roots[schema] ||= {})
109
+ base_uri = base_uri.to_s
110
+ cache_key = [base_uri, dialect]
111
+ root = roots[cache_key]
112
+ return root if root
113
+
114
+ roots[cache_key] = compile_atomically do |changes|
115
+ root_dialect = dialect_for(schema, dialect, changes)
116
+ compile_document(schema, base_uri, root_dialect, changes)
117
+ end
118
+ end
119
+
120
+ def meta_schema_root(schema)
121
+ SchemaDomain.validate!(schema)
122
+ uri = if schema.is_a?(Hash) && schema.key?("$schema")
123
+ schema["$schema"].to_s
124
+ else
125
+ @default_dialect.uri
126
+ end
127
+ root = (@meta_schema_roots[uri] ||= compile({"$ref" => uri}))
128
+ resolve(root, uri)
129
+ root
130
+ end
131
+
132
+ def resolve(node, reference)
133
+ if (resolved = @resolved_refs&.[](node))&.key?(reference)
134
+ return resolved[reference]
135
+ end
136
+
137
+ if Ractor.shareable?(self)
138
+ raise ResolutionError, "reference #{reference.inspect} was not resolved before sharing"
139
+ end
140
+
141
+ target = resolve_uncached(node, reference)
142
+ ((@resolved_refs ||= {})[node] ||= {})[reference] = target
143
+ end
144
+
145
+ def dynamic_anchor(resource, name)
146
+ @dynamic_anchors.dig(resource, name)
147
+ end
148
+
149
+ # Anchor tables grow in insertion order, including late external indexing.
150
+ # Native compilation uses this snapshot to avoid absent pair lookups.
151
+ def dynamic_anchor_names(resource)
152
+ @dynamic_anchors[resource]&.keys || []
153
+ end
154
+
155
+ def dynamic_scope?
156
+ @dynamic_scope
157
+ end
158
+
159
+ def make_shareable
160
+ return self if Ractor.shareable?(self)
161
+
162
+ @external_schemas.each_key do |uri|
163
+ index_external(strip_fragment(uri.to_s), @default_dialect)
164
+ end
165
+
166
+ index = 0
167
+ while index < nodes.length
168
+ node = nodes[index]
169
+ schema = node.schema
170
+ if schema.is_a?(Hash)
171
+ ["$ref", "$recursiveRef", "$dynamicRef"].each do |keyword|
172
+ resolve(node, schema[keyword]) if schema.key?(keyword)
173
+ end
174
+ end
175
+ index += 1
176
+ end
177
+
178
+ @resolved_refs ||= {}
179
+ Ractor.make_shareable(self)
180
+ end
181
+
182
+ private def compilation_changes
183
+ CompilationChanges.new
184
+ end
185
+
186
+ private def compile_atomically
187
+ changes = compilation_changes
188
+ result = yield changes
189
+ commit(changes)
190
+ result
191
+ end
192
+
193
+ private def commit(changes)
194
+ resources.update(changes.resources)
195
+ changes.resource_nodes.each do |resource, resource_nodes|
196
+ resource_nodes.each_value { |node| resource.add(node) }
197
+ end
198
+ uri_registry.update(changes.uri_registry)
199
+ nodes.concat(changes.nodes)
200
+ if @nodes_by_document_location
201
+ changes.document_locations.each do |document_key, schema_path, node|
202
+ (@nodes_by_document_location[document_key] ||= {})[schema_path] = node
203
+ end
204
+ end
205
+ changes.dynamic_anchors.each do |resource, anchors|
206
+ merged = (@dynamic_anchors[resource] ||= {}).update(anchors)
207
+ resource.dynamic_anchor_count = merged.size
208
+ end
209
+ (@custom_dialects ||= {}).update(changes.custom_dialects) unless changes.custom_dialects.empty?
210
+ @dynamic_scope = true if changes.requires_dynamic_scope
211
+ end
212
+
213
+ private def resolve_uncached(node, reference)
214
+ uri = absolute_uri(node.base_uri, reference)
215
+ document_uri = strip_fragment(uri)
216
+ index_external(document_uri, node.dialect)
217
+
218
+ if (target = uri_registry[uri])
219
+ return target
220
+ end
221
+
222
+ resource = if node.resource.uri == document_uri
223
+ node.resource
224
+ else
225
+ resources[document_uri]
226
+ end
227
+ raise ResolutionError, "unresolvable reference #{reference.inspect}" unless resource
228
+
229
+ raw_fragment = fragment(uri)
230
+ return resource.root if raw_fragment.empty?
231
+
232
+ decoded = URI.decode_uri_component(raw_fragment)
233
+ unless decoded.start_with?("/")
234
+ raise ResolutionError, "unsupported plain-name fragment ##{raw_fragment}"
235
+ end
236
+
237
+ pointer = canonical_pointer(decoded)
238
+ return resource.node_at(pointer) if resource.node_at(pointer)
239
+
240
+ schema_path = "#{resource.root.schema_path}#{pointer}"
241
+ if (target = node_by_document_location(resource.root.document_key, schema_path))
242
+ return target
243
+ end
244
+
245
+ target = pointer_target(resource.root.schema, pointer)
246
+ compile_atomically do |changes|
247
+ compile_node(
248
+ target,
249
+ resource.root.base_uri,
250
+ resource.root.dialect,
251
+ schema_path,
252
+ pointer,
253
+ resource,
254
+ resource.root.document_key,
255
+ changes
256
+ )
257
+ end
258
+ rescue URI::Error
259
+ raise ResolutionError, "unresolvable reference #{reference.inspect}"
260
+ end
261
+
262
+ def node_at(uri)
263
+ uri_registry[uri.to_s]
264
+ end
265
+
266
+ private def compile_document(schema, retrieval_uri, dialect, changes)
267
+ document_key = retrieval_uri.empty? ? Object.new : retrieval_uri
268
+ root = compile_node(schema, retrieval_uri, dialect, "", "", nil, document_key, changes)
269
+ document_uri = strip_fragment(retrieval_uri)
270
+ register_resource(document_uri, root, changes) unless document_uri.empty? || resource_at(document_uri, changes)
271
+ register_uri_unless_present(retrieval_uri, root, changes) unless retrieval_uri.empty?
272
+ register_uri_unless_present(document_uri, root, changes) unless document_uri.empty?
273
+ root
274
+ end
275
+
276
+ private def compile_node(schema, inherited_base, dialect, schema_path, resource_path, resource, document_key, changes)
277
+ hash_schema = schema.is_a?(Hash)
278
+ if hash_schema && (schema.key?("$recursiveRef") || schema.key?("$dynamicRef"))
279
+ changes.requires_dynamic_scope = true
280
+ end
281
+ dialect = dialect_for(schema, dialect, changes) if hash_schema && schema.key?("$schema")
282
+ if hash_schema && dialect.format_assertion? && schema.key?("format") &&
283
+ Formats.resolve(schema["format"]).name.nil?
284
+ raise UnsupportedFormatError,
285
+ "unsupported format #{schema["format"].inspect} required by Format-Assertion vocabulary"
286
+ end
287
+ exclusive_ref = hash_schema && schema.key?("$ref") && !dialect.ref_siblings?
288
+ base = if hash_schema && !exclusive_ref && schema.key?("$id")
289
+ absolute_uri(inherited_base, schema["$id"])
290
+ else
291
+ inherited_base
292
+ end
293
+ base = inherited_base if base.empty?
294
+ starts_resource = hash_schema && schema.key?("$id") && !exclusive_ref &&
295
+ !base.empty? && fragment(base).empty?
296
+ resource_path = "" if starts_resource
297
+
298
+ node = SchemaNode.new(
299
+ schema: schema,
300
+ dialect: dialect,
301
+ base_uri: base,
302
+ schema_path: schema_path,
303
+ resource_path: resource_path,
304
+ document_key: document_key
305
+ )
306
+ changes.nodes << node
307
+ if @nodes_by_document_location
308
+ changes.mutable_document_locations << [document_key, schema_path, node]
309
+ end
310
+
311
+ if hash_schema && schema.key?("$id") && !exclusive_ref && !base.empty?
312
+ changes.mutable_uri_registry[base] = node
313
+ if starts_resource
314
+ resource = register_resource(strip_fragment(base), node, changes)
315
+ end
316
+ end
317
+
318
+ resource ||= register_resource(strip_fragment(base), node, changes)
319
+ unless node.resource
320
+ node.resource = resource
321
+ (changes.mutable_resource_nodes[resource] ||= {})[node.resource_path] = node unless node.equal?(resource.root)
322
+ end
323
+
324
+ if hash_schema && !exclusive_ref
325
+ register_anchor(node, resource, schema["$anchor"], changes) if schema.key?("$anchor")
326
+ if schema.key?("$dynamicAnchor")
327
+ register_anchor(node, resource, schema["$dynamicAnchor"], changes, dynamic: true)
328
+ end
329
+ end
330
+
331
+ dialect.each_subschema(schema) do |child_schema, segments|
332
+ child_schema_path = append_segments(schema_path, segments)
333
+ child_resource_path = if resource_path == schema_path
334
+ child_schema_path
335
+ else
336
+ append_segments(resource_path, segments)
337
+ end
338
+ child = compile_node(
339
+ child_schema,
340
+ base,
341
+ dialect,
342
+ child_schema_path,
343
+ child_resource_path,
344
+ resource,
345
+ document_key,
346
+ changes
347
+ )
348
+ node.add_child(*segments, child: child)
349
+ end
350
+ node.freeze
351
+ end
352
+
353
+ private def register_resource(uri, root, changes)
354
+ return Resource.new(uri, root) if uri.empty?
355
+
356
+ resource = resource_at(uri, changes)
357
+ return resource if resource && resource.root.equal?(root)
358
+ return resource if resource
359
+
360
+ changes.mutable_resources[uri] = Resource.new(uri, root)
361
+ end
362
+
363
+ private def resource_at(uri, changes)
364
+ changes.resources.fetch(uri) { resources[uri] }
365
+ end
366
+
367
+ private def register_uri_unless_present(uri, node, changes)
368
+ return if changes.uri_registry.key?(uri) || uri_registry.key?(uri)
369
+
370
+ changes.mutable_uri_registry[uri] = node
371
+ end
372
+
373
+ private def register_anchor(node, resource, name, changes, dynamic: false)
374
+ return unless name.is_a?(String) && !name.empty?
375
+
376
+ changes.mutable_uri_registry["#{resource.uri}##{name}"] = node
377
+ ((changes.mutable_dynamic_anchors[resource] ||= {})[name] = node) if dynamic
378
+ end
379
+
380
+ private def node_by_document_location(document_key, schema_path)
381
+ unless @nodes_by_document_location
382
+ target = nodes.find { |node| node.document_key == document_key && node.schema_path == schema_path }
383
+ return unless target
384
+
385
+ @nodes_by_document_location = nodes.each_with_object({}) do |node, result|
386
+ (result[node.document_key] ||= {})[node.schema_path] = node
387
+ end
388
+ end
389
+ @nodes_by_document_location.dig(document_key, schema_path)
390
+ end
391
+
392
+ private def index_external(document_uri, fallback_dialect)
393
+ return if @indexed_external_schemas&.[](document_uri)
394
+
395
+ if (meta_schema = MetaSchemas.resolve(document_uri))
396
+ dialect = Dialect.resolve(meta_schema["$schema"]) || fallback_dialect
397
+ compile_atomically do |changes|
398
+ compile_document(meta_schema, document_uri, dialect, changes)
399
+ end
400
+ (@indexed_external_schemas ||= {})[document_uri] = true
401
+ return
402
+ end
403
+ if @external_schemas.key?(document_uri)
404
+ external_schema = @external_schemas[document_uri]
405
+ compile_atomically do |changes|
406
+ dialect = dialect_for(external_schema, fallback_dialect, changes)
407
+ compile_document(external_schema, document_uri, dialect, changes)
408
+ end
409
+ (@indexed_external_schemas ||= {})[document_uri] = true
410
+ return
411
+ end
412
+
413
+ matches = @external_schemas.select do |external_uri, _schema|
414
+ strip_fragment(external_uri.to_s) == document_uri
415
+ end
416
+ compile_atomically do |changes|
417
+ matches.each do |external_uri, external_schema|
418
+ external_uri = external_uri.to_s
419
+ dialect = dialect_for(external_schema, fallback_dialect, changes)
420
+ compile_document(external_schema, external_uri, dialect, changes)
421
+ end
422
+ end
423
+ (@indexed_external_schemas ||= {})[document_uri] = true
424
+ end
425
+
426
+ private def dialect_for(schema, fallback, changes = nil)
427
+ return fallback unless schema.is_a?(Hash) && schema.key?("$schema")
428
+
429
+ uri = schema["$schema"].to_s
430
+ Dialect.resolve(uri) || custom_dialect(uri, fallback, changes) || fallback
431
+ end
432
+
433
+ private def custom_dialect(uri, fallback, changes)
434
+ meta_schema = @external_schemas[uri] || @external_schemas[uri.delete_suffix("#")]
435
+ return unless meta_schema.is_a?(Hash) && meta_schema["$vocabulary"].is_a?(Hash)
436
+
437
+ custom_dialects = changes ? changes.mutable_custom_dialects : (@custom_dialects ||= {})
438
+ return custom_dialects[uri] if custom_dialects.key?(uri)
439
+ return @custom_dialects[uri] if @custom_dialects&.key?(uri)
440
+
441
+ custom_dialects[uri] ||= begin
442
+ vocabulary = meta_schema["$vocabulary"]
443
+ validation = vocabulary.any? { |name, enabled| enabled && name.end_with?("/validation") }
444
+ format_assertion = vocabulary.any? { |name, _| name.end_with?("/format-assertion") }
445
+ keywords = if validation
446
+ fallback.keywords
447
+ else
448
+ fallback.keywords.except(*VALIDATION_KEYWORDS)
449
+ end
450
+ if format_assertion
451
+ keywords = keywords.merge("format" => Dialect::Keyword.new(mask: Dialect::STRING))
452
+ end
453
+ Dialect.new(
454
+ name: fallback.name,
455
+ uri: uri,
456
+ keywords: keywords,
457
+ ref_siblings: fallback.ref_siblings?,
458
+ format_assertion: format_assertion
459
+ )
460
+ end
461
+ end
462
+
463
+ VALIDATION_KEYWORDS = %w[
464
+ type enum const multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
465
+ maxLength minLength pattern maxItems minItems uniqueItems maxContains minContains
466
+ maxProperties minProperties required dependentRequired
467
+ ].freeze
468
+ private_constant :VALIDATION_KEYWORDS
469
+
470
+ private def absolute_uri(base, identifier)
471
+ base = base.to_s
472
+ return base if identifier.nil?
473
+
474
+ identifier = identifier.to_s
475
+ return identifier if base.empty? || identifier.match?(/\A[A-Za-z][A-Za-z0-9+.-]*:/)
476
+ return strip_fragment(base) if identifier.empty?
477
+ return "#{strip_fragment(base)}#{identifier}" if identifier.start_with?("#")
478
+
479
+ URI.join(base, identifier).to_s
480
+ rescue URI::Error
481
+ begin
482
+ URI.join("resolve:///", base, identifier).to_s.delete_prefix("resolve:///")
483
+ rescue URI::Error
484
+ identifier
485
+ end
486
+ end
487
+
488
+ private def strip_fragment(uri)
489
+ index = uri.index("#")
490
+ index ? uri[0, index] : uri
491
+ end
492
+
493
+ private def fragment(uri)
494
+ index = uri.index("#")
495
+ index ? uri[(index + 1)..] : ""
496
+ end
497
+
498
+ private def append_segments(path, segments)
499
+ segments.reduce(path) do |result, segment|
500
+ encoded = segment.to_s
501
+ encoded = encoded.gsub("~", "~0") if encoded.include?("~")
502
+ encoded = encoded.gsub("/", "~1") if encoded.include?("/")
503
+ "#{result}/#{encoded}"
504
+ end
505
+ end
506
+
507
+ private def canonical_pointer(pointer)
508
+ return "" if pointer.empty?
509
+
510
+ tokens = pointer.split("/", -1).drop(1).map do |token|
511
+ decoded = token.gsub("~1", "/").gsub("~0", "~")
512
+ decoded.gsub("~", "~0").gsub("/", "~1")
513
+ end
514
+ "/#{tokens.join("/")}"
515
+ end
516
+
517
+ private def pointer_target(document, pointer)
518
+ pointer.split("/", -1).drop(1).reduce(document) do |current, token|
519
+ key = token.gsub("~1", "/").gsub("~0", "~")
520
+ if current.is_a?(Array)
521
+ unless key.match?(/\A(?:0|[1-9]\d*)\z/)
522
+ raise ResolutionError, "invalid JSON Pointer index #{key.inspect}"
523
+ end
524
+ current.fetch(key.to_i)
525
+ elsif current.is_a?(Hash)
526
+ current.fetch(key)
527
+ else
528
+ raise ResolutionError, "JSON Pointer traverses a scalar"
529
+ end
530
+ end
531
+ rescue IndexError
532
+ raise ResolutionError, "JSON Pointer target does not exist"
533
+ end
534
+ end
535
+ end
536
+
537
+ private_constant :Internal
538
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "formats"
4
+
5
+ module Schemurai
6
+ module Internal
7
+ class SchemaNode
8
+ MISSING_SEGMENT = Object.new.freeze
9
+
10
+ attr_reader :schema, :dialect, :base_uri, :schema_path, :resource_path,
11
+ :keyword_mask, :document_key, :format
12
+ attr_accessor :resource
13
+
14
+ def initialize(schema:, dialect:, base_uri:, schema_path:, resource_path:, document_key:)
15
+ @schema = schema
16
+ @dialect = dialect
17
+ @base_uri = base_uri
18
+ @schema_path = schema_path
19
+ @resource_path = resource_path
20
+ @document_key = document_key
21
+ @keyword_mask = schema.is_a?(Hash) ? dialect.keyword_mask(schema) : 0
22
+ @format = Formats.resolve(schema["format"]) if schema.is_a?(Hash) && schema.key?("format")
23
+ @children = nil
24
+ end
25
+
26
+ def add_child(keyword, segment = MISSING_SEGMENT, child:)
27
+ children = (@children ||= {})
28
+ if segment.equal?(MISSING_SEGMENT)
29
+ children[keyword] = child
30
+ else
31
+ (children[keyword] ||= {})[segment] = child
32
+ end
33
+ end
34
+
35
+ def child(keyword, segment = MISSING_SEGMENT)
36
+ return unless @children
37
+ return @children[keyword] if segment.equal?(MISSING_SEGMENT)
38
+
39
+ @children.dig(keyword, segment)
40
+ end
41
+
42
+ def freeze
43
+ if @children
44
+ @children.each_value { |value| value.freeze if value.is_a?(Hash) }
45
+ @children.freeze
46
+ end
47
+ super
48
+ end
49
+
50
+ private_constant :MISSING_SEGMENT
51
+ end
52
+ end
53
+
54
+ private_constant :Internal
55
+ end