jsi-dev 0.0.0.pre.commonmarker

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 (85) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +8 -0
  3. data/CHANGELOG.md +101 -0
  4. data/LICENSE.md +613 -0
  5. data/README.md +303 -0
  6. data/docs/glossary.md +281 -0
  7. data/jsi.gemspec +30 -0
  8. data/lib/jsi/base/node.rb +373 -0
  9. data/lib/jsi/base.rb +738 -0
  10. data/lib/jsi/jsi_coder.rb +92 -0
  11. data/lib/jsi/metaschema.rb +6 -0
  12. data/lib/jsi/metaschema_node/bootstrap_schema.rb +126 -0
  13. data/lib/jsi/metaschema_node.rb +262 -0
  14. data/lib/jsi/ptr.rb +314 -0
  15. data/lib/jsi/schema/application/child_application/contains.rb +25 -0
  16. data/lib/jsi/schema/application/child_application/draft04.rb +21 -0
  17. data/lib/jsi/schema/application/child_application/draft06.rb +28 -0
  18. data/lib/jsi/schema/application/child_application/draft07.rb +28 -0
  19. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  20. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  21. data/lib/jsi/schema/application/child_application.rb +13 -0
  22. data/lib/jsi/schema/application/draft04.rb +8 -0
  23. data/lib/jsi/schema/application/draft06.rb +8 -0
  24. data/lib/jsi/schema/application/draft07.rb +8 -0
  25. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  26. data/lib/jsi/schema/application/inplace_application/draft04.rb +25 -0
  27. data/lib/jsi/schema/application/inplace_application/draft06.rb +26 -0
  28. data/lib/jsi/schema/application/inplace_application/draft07.rb +32 -0
  29. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  30. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  31. data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
  32. data/lib/jsi/schema/application/inplace_application.rb +14 -0
  33. data/lib/jsi/schema/application.rb +12 -0
  34. data/lib/jsi/schema/draft04.rb +13 -0
  35. data/lib/jsi/schema/draft06.rb +13 -0
  36. data/lib/jsi/schema/draft07.rb +13 -0
  37. data/lib/jsi/schema/issue.rb +36 -0
  38. data/lib/jsi/schema/ref.rb +183 -0
  39. data/lib/jsi/schema/schema_ancestor_node.rb +122 -0
  40. data/lib/jsi/schema/validation/array.rb +69 -0
  41. data/lib/jsi/schema/validation/const.rb +20 -0
  42. data/lib/jsi/schema/validation/contains.rb +25 -0
  43. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  44. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  45. data/lib/jsi/schema/validation/draft04.rb +110 -0
  46. data/lib/jsi/schema/validation/draft06.rb +120 -0
  47. data/lib/jsi/schema/validation/draft07.rb +157 -0
  48. data/lib/jsi/schema/validation/enum.rb +25 -0
  49. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  50. data/lib/jsi/schema/validation/items.rb +54 -0
  51. data/lib/jsi/schema/validation/not.rb +20 -0
  52. data/lib/jsi/schema/validation/numeric.rb +121 -0
  53. data/lib/jsi/schema/validation/object.rb +45 -0
  54. data/lib/jsi/schema/validation/pattern.rb +34 -0
  55. data/lib/jsi/schema/validation/properties.rb +101 -0
  56. data/lib/jsi/schema/validation/property_names.rb +32 -0
  57. data/lib/jsi/schema/validation/ref.rb +40 -0
  58. data/lib/jsi/schema/validation/required.rb +27 -0
  59. data/lib/jsi/schema/validation/someof.rb +90 -0
  60. data/lib/jsi/schema/validation/string.rb +47 -0
  61. data/lib/jsi/schema/validation/type.rb +49 -0
  62. data/lib/jsi/schema/validation.rb +49 -0
  63. data/lib/jsi/schema.rb +792 -0
  64. data/lib/jsi/schema_classes.rb +357 -0
  65. data/lib/jsi/schema_registry.rb +190 -0
  66. data/lib/jsi/schema_set.rb +219 -0
  67. data/lib/jsi/simple_wrap.rb +26 -0
  68. data/lib/jsi/util/private/attr_struct.rb +130 -0
  69. data/lib/jsi/util/private/memo_map.rb +75 -0
  70. data/lib/jsi/util/private.rb +202 -0
  71. data/lib/jsi/util/typelike.rb +225 -0
  72. data/lib/jsi/util.rb +227 -0
  73. data/lib/jsi/validation/error.rb +34 -0
  74. data/lib/jsi/validation/result.rb +212 -0
  75. data/lib/jsi/validation.rb +15 -0
  76. data/lib/jsi/version.rb +5 -0
  77. data/lib/jsi.rb +105 -0
  78. data/lib/schemas/json-schema.org/draft-04/schema.rb +169 -0
  79. data/lib/schemas/json-schema.org/draft-06/schema.rb +171 -0
  80. data/lib/schemas/json-schema.org/draft-07/schema.rb +198 -0
  81. data/readme.rb +138 -0
  82. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  83. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  84. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  85. metadata +155 -0
@@ -0,0 +1,357 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ # A Module associated with a JSI Schema. See {Schema#jsi_schema_module}.
5
+ class SchemaModule < Module
6
+ # @private
7
+ def initialize(schema, &block)
8
+ super(&block)
9
+
10
+ @jsi_node = schema
11
+
12
+ schema.jsi_schemas.each do |schema_schema|
13
+ extend SchemaClasses.schema_property_reader_module(schema_schema, conflicting_modules: Set[SchemaModule])
14
+ end
15
+ end
16
+
17
+ # The schema for which this is the JSI Schema Module
18
+ # @return [Base + Schema]
19
+ def schema
20
+ @jsi_node
21
+ end
22
+
23
+ # a URI which refers to the schema. see {Schema#schema_uri}.
24
+ # @return (see Schema#schema_uri)
25
+ def schema_uri
26
+ schema.schema_uri
27
+ end
28
+
29
+ # @return [String]
30
+ def inspect
31
+ if name_from_ancestor
32
+ if schema.schema_absolute_uri
33
+ -"#{name_from_ancestor} <#{schema.schema_absolute_uri}> (JSI Schema Module)"
34
+ else
35
+ -"#{name_from_ancestor} (JSI Schema Module)"
36
+ end
37
+ else
38
+ -"(JSI Schema Module: #{schema.schema_uri || schema.jsi_ptr.uri})"
39
+ end
40
+ end
41
+
42
+ alias_method :to_s, :inspect
43
+
44
+ # invokes {JSI::Schema#new_jsi} on this module's schema, passing the given instance.
45
+ #
46
+ # @param (see JSI::Schema#new_jsi)
47
+ # @return [JSI::Base subclass] a JSI whose content comes from the given instance and whose schemas are
48
+ # inplace applicators of this module's schema.
49
+ def new_jsi(instance, **kw)
50
+ schema.new_jsi(instance, **kw)
51
+ end
52
+
53
+ # See {Schema#schema_content}
54
+ def schema_content
55
+ schema.jsi_node_content
56
+ end
57
+
58
+ # See {Schema#instance_validate}
59
+ def instance_validate(instance)
60
+ schema.instance_validate(instance)
61
+ end
62
+
63
+ # See {Schema#instance_valid?}
64
+ def instance_valid?(instance)
65
+ schema.instance_valid?(instance)
66
+ end
67
+ end
68
+
69
+ # A module to extend the {SchemaModule} of a schema which describes other schemas (a {Schema::DescribesSchema})
70
+ module SchemaModule::DescribesSchemaModule
71
+ # Instantiates the given schema content as a JSI Schema.
72
+ #
73
+ # see {JSI::Schema::DescribesSchema#new_schema}
74
+ #
75
+ # @param (see JSI::Schema::DescribesSchema#new_schema)
76
+ # @yield (see JSI::Schema::DescribesSchema#new_schema)
77
+ # @return [JSI::Base subclass + JSI::Schema] a JSI which is a {JSI::Schema} whose content comes from
78
+ # the given `schema_content` and whose schemas are inplace applicators of this module's schema
79
+ def new_schema(schema_content, **kw, &block)
80
+ schema.new_schema(schema_content, **kw, &block)
81
+ end
82
+
83
+ # (see Schema::DescribesSchema#new_schema_module)
84
+ def new_schema_module(schema_content, **kw, &block)
85
+ schema.new_schema(schema_content, **kw, &block).jsi_schema_module
86
+ end
87
+
88
+ # @return [Set<Module>]
89
+ attr_reader :schema_implementation_modules
90
+ end
91
+
92
+ # this module is a namespace for building schema classes and schema modules.
93
+ module SchemaClasses
94
+ class << self
95
+ # @api private
96
+ # @return [Set<Module>]
97
+ def includes_for(instance)
98
+ includes = Set[]
99
+ includes << Base::ArrayNode if instance.respond_to?(:to_ary)
100
+ includes << Base::HashNode if instance.respond_to?(:to_hash)
101
+ includes << Base::StringNode if instance.respond_to?(:to_str)
102
+ includes.freeze
103
+ end
104
+
105
+ # a JSI Schema Class which represents the given schemas.
106
+ # an instance of the class is a JSON Schema instance described by all of the given schemas.
107
+ # @api private
108
+ # @param schemas [Enumerable<JSI::Schema>] schemas which the class will represent
109
+ # @param includes [Enumerable<Module>] modules which will be included on the class
110
+ # @return [Class subclassing JSI::Base]
111
+ def class_for_schemas(schemas, includes: )
112
+ @class_for_schemas_map[
113
+ schemas: SchemaSet.ensure_schema_set(schemas),
114
+ includes: Util.ensure_module_set(includes),
115
+ ]
116
+ end
117
+
118
+ private def class_for_schemas_compute(schemas: , includes: )
119
+ Class.new(Base) do
120
+ define_singleton_method(:jsi_class_schemas) { schemas }
121
+ define_method(:jsi_schemas) { schemas }
122
+
123
+ define_singleton_method(:jsi_class_includes) { includes }
124
+
125
+ conflicting_modules = Set[JSI::Base] + includes + schemas.map(&:jsi_schema_module)
126
+
127
+ reader_modules = schemas.map do |schema|
128
+ JSI::SchemaClasses.schema_property_reader_module(schema, conflicting_modules: conflicting_modules)
129
+ end
130
+ reader_modules.each { |m| include m }
131
+ readers = reader_modules.map(&:jsi_property_readers).inject(Set[], &:merge).freeze
132
+ define_method(:jsi_property_readers) { readers }
133
+ define_singleton_method(:jsi_property_readers) { readers }
134
+
135
+ writer_modules = schemas.map do |schema|
136
+ JSI::SchemaClasses.schema_property_writer_module(schema, conflicting_modules: conflicting_modules)
137
+ end
138
+ writer_modules.each { |m| include m }
139
+
140
+ includes.each { |m| include(m) }
141
+ schemas.each { |schema| include(schema.jsi_schema_module) }
142
+ jsi_class = self
143
+ define_method(:jsi_class) { jsi_class }
144
+
145
+ self
146
+ end
147
+ end
148
+
149
+ # a subclass of MetaschemaNode::BootstrapSchema with the given modules included
150
+ # @api private
151
+ # @param modules [Set<Module>] schema implementation modules
152
+ # @return [Class]
153
+ def bootstrap_schema_class(modules)
154
+ @bootstrap_schema_class_map[
155
+ modules: Util.ensure_module_set(modules),
156
+ ]
157
+ end
158
+
159
+ private def bootstrap_schema_class_compute(modules: )
160
+ Class.new(MetaschemaNode::BootstrapSchema) do
161
+ define_singleton_method(:schema_implementation_modules) { modules }
162
+ define_method(:schema_implementation_modules) { modules }
163
+ modules.each { |mod| include(mod) }
164
+
165
+ self
166
+ end
167
+ end
168
+
169
+ # see {Schema#jsi_schema_module}
170
+ # @api private
171
+ # @return [Module + SchemaModule]
172
+ def module_for_schema(schema)
173
+ Schema.ensure_schema(schema)
174
+ raise(Bug, "non-Base schema cannot have schema module: #{schema}") unless schema.is_a?(Base)
175
+ @schema_module_map[schema: schema]
176
+ end
177
+
178
+ private def schema_module_compute(schema: )
179
+ SchemaModule.new(schema)
180
+ end
181
+
182
+ # @deprecated after v0.7
183
+ def accessor_module_for_schema(schema, conflicting_modules: , setters: true)
184
+ Module.new do
185
+ include SchemaClasses.schema_property_reader_module(schema, conflicting_modules: conflicting_modules)
186
+ include SchemaClasses.schema_property_writer_module(schema, conflicting_modules: conflicting_modules) if setters
187
+ end
188
+ end
189
+
190
+ # a module of readers for described property names of the given schema.
191
+ #
192
+ # @api private
193
+ # @param schema [JSI::Schema] a schema for which to define readers for any described property names
194
+ # @param conflicting_modules [Enumerable<Module>] an array of modules (or classes) which
195
+ # may be used alongside the accessor module. methods defined by any conflicting_module
196
+ # will not be defined as accessors.
197
+ # @return [Module]
198
+ def schema_property_reader_module(schema, conflicting_modules: )
199
+ Schema.ensure_schema(schema)
200
+ @schema_property_reader_module_map[schema: schema, conflicting_modules: conflicting_modules]
201
+ end
202
+
203
+ private def schema_property_reader_module_compute(schema: , conflicting_modules: )
204
+ Module.new do
205
+ define_singleton_method(:inspect) { '(JSI Schema Property Reader Module)' }
206
+
207
+ readers = schema.described_object_property_names.select do |name|
208
+ Util.ok_ruby_method_name?(name) &&
209
+ !conflicting_modules.any? { |m| m.method_defined?(name) || m.private_method_defined?(name) }
210
+ end.to_set.freeze
211
+
212
+ define_singleton_method(:jsi_property_readers) { readers }
213
+
214
+ readers.each do |property_name|
215
+ define_method(property_name) do |**kw, &block|
216
+ self[property_name, **kw, &block]
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ # a module of writers for described property names of the given schema.
223
+ # @api private
224
+ def schema_property_writer_module(schema, conflicting_modules: )
225
+ Schema.ensure_schema(schema)
226
+ @schema_property_writer_module_map[schema: schema, conflicting_modules: conflicting_modules]
227
+ end
228
+
229
+ private def schema_property_writer_module_compute(schema: , conflicting_modules: )
230
+ Module.new do
231
+ define_singleton_method(:inspect) { '(JSI Schema Property Writer Module)' }
232
+
233
+ writers = schema.described_object_property_names.select do |name|
234
+ writer = "#{name}="
235
+ Util.ok_ruby_method_name?(name) &&
236
+ !conflicting_modules.any? { |m| m.method_defined?(writer) || m.private_method_defined?(writer) }
237
+ end.to_set.freeze
238
+
239
+ define_singleton_method(:jsi_property_writers) { writers }
240
+
241
+ writers.each do |property_name|
242
+ define_method("#{property_name}=") do |value|
243
+ self[property_name] = value
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
249
+
250
+ @class_for_schemas_map = Hash.new { |h, k| h[k] = class_for_schemas_compute(**k) }
251
+ @bootstrap_schema_class_map = Hash.new { |h, k| h[k] = bootstrap_schema_class_compute(**k) }
252
+ @schema_module_map = Hash.new { |h, k| h[k] = schema_module_compute(**k) }
253
+ @schema_property_reader_module_map = Hash.new { |h, k| h[k] = schema_property_reader_module_compute(**k) }
254
+ @schema_property_writer_module_map = Hash.new { |h, k| h[k] = schema_property_writer_module_compute(**k) }
255
+ end
256
+
257
+ # connecting {SchemaModule}s via {SchemaModule::Connection}s
258
+ module SchemaModule::Connects
259
+ attr_reader :jsi_node
260
+
261
+ # a name relative to a named schema module of an ancestor schema.
262
+ # for example, if `Foos = JSI::JSONSchemaDraft07.new_schema_module({'items' => {}})`
263
+ # then the module `Foos.items` will have a name_from_ancestor of `"Foos.items"`
264
+ # @api private
265
+ # @return [String, nil]
266
+ def name_from_ancestor
267
+ named_ancestor_schema, tokens = named_ancestor_schema_tokens
268
+ return nil unless named_ancestor_schema
269
+
270
+ name = named_ancestor_schema.jsi_schema_module.name
271
+ ancestor = named_ancestor_schema
272
+ tokens.each do |token|
273
+ if ancestor.jsi_property_readers.include?(token)
274
+ name += ".#{token}"
275
+ elsif [String, Numeric, TrueClass, FalseClass, NilClass].any? { |m| token.is_a?(m) }
276
+ name += "[#{token.inspect}]"
277
+ else
278
+ return nil
279
+ end
280
+ ancestor = ancestor[token]
281
+ end
282
+ name
283
+ end
284
+
285
+ # Subscripting a JSI schema module or a {SchemaModule::Connection} will subscript its node, and
286
+ # if the result is a JSI::Schema, return the JSI Schema module of that schema; if it is a JSI::Base,
287
+ # return a SchemaModule::Connection; or if it is another value (a basic type), return that value.
288
+ #
289
+ # @param token [Object]
290
+ # @yield If the token identifies a schema and a block is given,
291
+ # it is evaluated in the context of the schema's JSI schema module
292
+ # using [Module#module_exec](https://ruby-doc.org/core/Module.html#method-i-module_exec).
293
+ # @return [Module, SchemaModule::Connection, Object]
294
+ def [](token, **kw, &block)
295
+ raise(ArgumentError) unless kw.empty? # TODO remove eventually (keyword argument compatibility)
296
+ sub = @jsi_node[token]
297
+ if sub.is_a?(JSI::Schema)
298
+ sub.jsi_schema_module_exec(&block) if block
299
+ sub.jsi_schema_module
300
+ elsif block
301
+ raise(ArgumentError, "block given but token #{token.inspect} does not identify a schema")
302
+ elsif sub.is_a?(JSI::Base)
303
+ SchemaModule::Connection.new(sub)
304
+ else
305
+ sub
306
+ end
307
+ end
308
+
309
+ private
310
+
311
+ # @return [Array<JSI::Schema, Array>, nil]
312
+ def named_ancestor_schema_tokens
313
+ schema_ancestors = @jsi_node.jsi_ancestor_nodes
314
+ named_ancestor_schema = schema_ancestors.detect { |jsi| jsi.is_a?(JSI::Schema) && jsi.jsi_schema_module.name }
315
+ return nil unless named_ancestor_schema
316
+ tokens = @jsi_node.jsi_ptr.relative_to(named_ancestor_schema.jsi_ptr).tokens
317
+ [named_ancestor_schema, tokens]
318
+ end
319
+ end
320
+
321
+ class SchemaModule
322
+ include Connects
323
+ end
324
+
325
+ # A JSI Schema Module is a module which represents a schema. A SchemaModule::Connection represents
326
+ # a node in a schema's document which is not a schema, such as the 'properties'
327
+ # object (which contains schemas but is not a schema).
328
+ #
329
+ # instances of this class act as a stand-in to allow users to subscript or call property accessors on
330
+ # schema modules to refer to their subschemas' schema modules.
331
+ #
332
+ # A SchemaModule::Connection has readers for property names described by the node's schemas.
333
+ class SchemaModule::Connection
334
+ include SchemaModule::Connects
335
+
336
+ # @param node [JSI::Base]
337
+ def initialize(node)
338
+ raise(Bug, "node must be JSI::Base: #{node.pretty_inspect.chomp}") unless node.is_a?(JSI::Base)
339
+ raise(Bug, "node must not be JSI::Schema: #{node.pretty_inspect.chomp}") if node.is_a?(JSI::Schema)
340
+ @jsi_node = node
341
+ node.jsi_schemas.each do |schema|
342
+ extend(JSI::SchemaClasses.schema_property_reader_module(schema, conflicting_modules: [SchemaModule::Connection]))
343
+ end
344
+ end
345
+
346
+ # @return [String]
347
+ def inspect
348
+ if name_from_ancestor
349
+ -"#{name_from_ancestor} (#{self.class})"
350
+ else
351
+ -"(#{self.class}: #{@jsi_node.jsi_ptr.uri})"
352
+ end
353
+ end
354
+
355
+ alias_method :to_s, :inspect
356
+ end
357
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ class SchemaRegistry
5
+ # an exception raised when an attempt is made to register a resource using a URI which is already
6
+ # registered with another resource
7
+ class Collision < StandardError
8
+ end
9
+
10
+ # an exception raised when an attempt is made to access (register or find) a resource of the
11
+ # registry using a URI which is not absolute (it is a relative URI or it contains a fragment)
12
+ class NonAbsoluteURI < StandardError
13
+ end
14
+
15
+ # an exception raised when a URI we are looking for has not been registered
16
+ class ResourceNotFound < StandardError
17
+ attr_accessor :uri
18
+ end
19
+
20
+ def initialize
21
+ @resources = {}
22
+ @autoload_uris = {}
23
+ @resources_mutex = Mutex.new
24
+ end
25
+
26
+ # registers the given resource and/or schema resources it contains in the registry.
27
+ #
28
+ # each descendent node of the resource (including the resource itself) is registered if it is a schema
29
+ # that has an absolute URI (generally defined by the '$id' keyword).
30
+ #
31
+ # the given resource itself will be registered, whether or not it is a schema, if it is the root
32
+ # of its document and was instantiated with the option `uri` specified.
33
+ #
34
+ # @param resource [JSI::Base] a JSI containing resources to register
35
+ # @return [void]
36
+ def register(resource)
37
+ unless resource.is_a?(JSI::Base)
38
+ raise(ArgumentError, "resource must be a JSI::Base. got: #{resource.pretty_inspect.chomp}")
39
+ end
40
+ unless resource.is_a?(JSI::Schema) || resource.jsi_ptr.root?
41
+ # unsure, should this be allowed? the given JSI is not a "resource" as we define it, but
42
+ # if this check is removed it will just register any resources (schemas) below the given JSI.
43
+ raise(ArgumentError, "undefined behavior: registration of a JSI which is not a schema and is not at the root of a document")
44
+ end
45
+
46
+ # allow for registration of resources at the root of a document whether or not they are schemas.
47
+ # jsi_schema_base_uri at the root comes from the `uri` parameter to new_jsi / new_schema.
48
+ if resource.jsi_schema_base_uri && resource.jsi_ptr.root?
49
+ register_single(resource.jsi_schema_base_uri, resource)
50
+ end
51
+
52
+ resource.jsi_each_descendent_node do |node|
53
+ if node.is_a?(JSI::Schema) && node.schema_absolute_uri
54
+ register_single(node.schema_absolute_uri, node)
55
+ end
56
+ end
57
+
58
+ nil
59
+ end
60
+
61
+ # takes a URI identifying a resource to be loaded by the given block
62
+ # when a reference to the URI is followed.
63
+ #
64
+ # for example:
65
+ #
66
+ # JSI.schema_registry.autoload_uri('http://example.com/schema.json') do
67
+ # JSI.new_schema({
68
+ # '$schema' => 'http://json-schema.org/draft-07/schema#',
69
+ # '$id' => 'http://example.com/schema.json',
70
+ # 'title' => 'my schema',
71
+ # })
72
+ # end
73
+ #
74
+ # the block would normally load JSON from the filesystem or similar.
75
+ #
76
+ # @param uri [Addressable::URI]
77
+ # @yieldreturn [JSI::Base] a JSI instance containing the resource identified by the given uri
78
+ # @return [void]
79
+ def autoload_uri(uri, &block)
80
+ uri = ensure_uri_absolute(uri)
81
+ mutating
82
+ unless block
83
+ raise(ArgumentError, ["#{SchemaRegistry}#autoload_uri must be invoked with a block", "URI: #{uri}"].join("\n"))
84
+ end
85
+ if @autoload_uris.key?(uri)
86
+ raise(Collision, ["already registered URI for autoload", "URI: #{uri}", "loader: #{@autoload_uris[uri]}"].join("\n"))
87
+ end
88
+ @autoload_uris[uri] = block
89
+ nil
90
+ end
91
+
92
+ # @param uri [Addressable::URI, #to_str]
93
+ # @return [JSI::Base]
94
+ # @raise [JSI::SchemaRegistry::ResourceNotFound]
95
+ def find(uri)
96
+ uri = ensure_uri_absolute(uri)
97
+ if @autoload_uris.key?(uri)
98
+ autoloaded = @autoload_uris[uri].call
99
+ register(autoloaded)
100
+ @autoload_uris.delete(uri)
101
+ end
102
+ if !@resources.key?(uri)
103
+ if autoloaded
104
+ msg = [
105
+ "URI #{uri} was registered with autoload_uri but the result did not contain a resource with that URI.",
106
+ "the resource resulting from autoload_uri was:",
107
+ autoloaded.pretty_inspect.chomp,
108
+ ]
109
+ else
110
+ msg = ["URI #{uri} is not registered. registered URIs:", *(@resources.keys | @autoload_uris.keys)]
111
+ end
112
+ raise(ResourceNotFound.new(msg.join("\n")).tap { |e| e.uri = uri })
113
+ end
114
+ @resources[uri]
115
+ end
116
+
117
+ def inspect
118
+ [
119
+ '#<JSI::SchemaRegistry',
120
+ *[['autoload', @autoload_uris.keys], ['resources', @resources.keys]].map do |label, uris|
121
+ [
122
+ " #{label} (#{uris.size})#{uris.empty? ? "" : ":"}",
123
+ *uris.map do |uri|
124
+ " #{uri}"
125
+ end,
126
+ ]
127
+ end.inject([], &:+),
128
+ '>',
129
+ ].join("\n").freeze
130
+ end
131
+
132
+ alias_method :to_s, :inspect
133
+
134
+ def dup
135
+ self.class.new.tap do |reg|
136
+ @resources.each do |uri, resource|
137
+ reg.register_single(uri, resource)
138
+ end
139
+ @autoload_uris.each do |uri, autoload|
140
+ reg.autoload_uri(uri, &autoload)
141
+ end
142
+ end
143
+ end
144
+
145
+ def freeze
146
+ @resources.freeze
147
+ @autoload_uris.freeze
148
+ @resources_mutex = nil
149
+ super
150
+ end
151
+
152
+ protected
153
+ # @param uri [Addressable::URI]
154
+ # @param resource [JSI::Base]
155
+ # @return [void]
156
+ def register_single(uri, resource)
157
+ mutating
158
+ @resources_mutex.synchronize do
159
+ ensure_uri_absolute(uri)
160
+ if @resources.key?(uri)
161
+ if @resources[uri] != resource
162
+ raise(Collision, "URI collision on #{uri}.\nexisting:\n#{@resources[uri].pretty_inspect.chomp}\nnew:\n#{resource.pretty_inspect.chomp}")
163
+ end
164
+ else
165
+ @resources[uri] = resource
166
+ end
167
+ end
168
+ nil
169
+ end
170
+
171
+ private
172
+
173
+ def ensure_uri_absolute(uri)
174
+ uri = Util.uri(uri)
175
+ if uri.fragment
176
+ raise(NonAbsoluteURI, "#{self.class} only registers absolute URIs. cannot access URI with fragment: #{uri}")
177
+ end
178
+ if uri.relative?
179
+ raise(NonAbsoluteURI, "#{self.class} only registers absolute URIs. cannot access relative URI: #{uri}")
180
+ end
181
+ uri
182
+ end
183
+
184
+ def mutating
185
+ if frozen?
186
+ raise(FrozenError, "cannot modify frozen #{self.class}")
187
+ end
188
+ end
189
+ end
190
+ end