jsi 0.7.0 → 0.8.0

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