jsi 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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