jsi-dev 0.0.0.pre.maruku

Sign up to get free protection for your applications and to get access to all the features.
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