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
data/lib/jsi/base.rb CHANGED
@@ -1,40 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSI
4
- # JSI::Base is the base class of every JSI instance of a JSON schema.
4
+ # A JSI::Base instance represents a node in a JSON document (its {#jsi_document}) at a particular
5
+ # location (its {#jsi_ptr}), described by any number of JSON Schemas (its {#jsi_schemas}).
5
6
  #
6
- # instances are described by a set of one or more JSON schemas. JSI dynamically creates a subclass of
7
- # JSI::Base for each set of JSON schemas which describe an instance that is to be instantiated.
7
+ # JSI::Base is an abstract base class. The subclasses used to instantiate JSIs are dynamically created as
8
+ # needed for a given instance.
8
9
  #
9
- # a JSI instance of such a subclass represents a JSON schema instance described by that set of schemas.
10
+ # These subclasses are generally intended to be ignored by applications using this library - the purpose
11
+ # they serve is to include modules relevant to the instance. The modules these classes include are:
10
12
  #
11
- # this subclass includes the JSI Schema Module of each schema it represents.
12
- #
13
- # the method {Base#jsi_schemas} is defined to indicate the schemas the class represents.
14
- #
15
- # the JSI::Base class itself is not intended to be instantiated.
13
+ # - the {Schema#jsi_schema_module} of each schema which describes the instance
14
+ # - {Base::HashNode}, {Base::ArrayNode}, or {Base::StringNode} if the instance is
15
+ # a hash/object, array, or string
16
+ # - Modules defining accessor methods for property names described by the schemas
16
17
  class Base
17
18
  autoload :ArrayNode, 'jsi/base/node'
18
19
  autoload :HashNode, 'jsi/base/node'
20
+ autoload :StringNode, 'jsi/base/node'
21
+ autoload(:Mutable, 'jsi/base/mutability')
22
+ autoload(:Immutable, 'jsi/base/mutability')
19
23
 
20
24
  include Schema::SchemaAncestorNode
21
- include Util::Memoize
22
25
 
23
- # an exception raised when {Base#[]} is invoked on an instance which is not an array or hash
24
- class CannotSubscriptError < StandardError
26
+ # An exception raised when attempting to access a child of a node which cannot have children.
27
+ # A complex node can have children, a simple node cannot.
28
+ class SimpleNodeChildError < StandardError
25
29
  end
26
30
 
27
31
  class << self
28
- # @private
29
- # is the constant JSI::SchemaClasses::<self.schema_classes_const_name> defined?
30
- # (if so, we will prefer to use something more human-readable than that ugly mess.)
31
- def in_schema_classes
32
- # #name sets @in_schema_classes
33
- name
34
- @in_schema_classes
35
- end
36
-
37
- # a string indicating a class name if one is defined, as well as the schema module name
32
+ # A string indicating the schema module name
38
33
  # and/or schema URI of each schema the class represents.
39
34
  # @return [String]
40
35
  def inspect
@@ -42,11 +37,11 @@ module JSI
42
37
  super
43
38
  else
44
39
  schema_names = jsi_class_schemas.map do |schema|
45
- mod = schema.jsi_schema_module
46
- if mod.name && schema.schema_uri
47
- "#{mod.name} (#{schema.schema_uri})"
48
- elsif mod.name
49
- mod.name
40
+ mod_name = schema.jsi_schema_module.name_from_ancestor
41
+ if mod_name && schema.schema_absolute_uri
42
+ "#{mod_name} <#{schema.schema_absolute_uri}>"
43
+ elsif mod_name
44
+ mod_name
50
45
  elsif schema.schema_uri
51
46
  schema.schema_uri.to_s
52
47
  else
@@ -54,61 +49,51 @@ module JSI
54
49
  end
55
50
  end
56
51
 
57
- if name && !in_schema_classes
58
- if jsi_class_schemas.empty?
59
- "#{name} (0 schemas)"
60
- else
61
- "#{name} (#{schema_names.join(', ')})"
62
- end
52
+ if schema_names.empty?
53
+ "(JSI Schema Class for 0 schemas#{jsi_class_includes.map { |n| " + #{n}" }})"
63
54
  else
64
- if schema_names.empty?
65
- "(JSI Schema Class for 0 schemas)"
66
- else
67
- "(JSI Schema Class: #{schema_names.join(', ')})"
68
- end
55
+ -"(JSI Schema Class: #{(schema_names + jsi_class_includes.map(&:name)).join(' + ')})"
69
56
  end
70
57
  end
71
58
  end
72
59
 
73
- alias_method :to_s, :inspect
74
-
75
- # @private
76
- # see {.name}
77
- def schema_classes_const_name
78
- if respond_to?(:jsi_class_schemas)
79
- schema_names = jsi_class_schemas.map do |schema|
80
- named_ancestor_schema, tokens = schema.jsi_schema_module.send(:named_ancestor_schema_tokens)
81
- if named_ancestor_schema
82
- [named_ancestor_schema.jsi_schema_module.name, *tokens].join('_')
83
- elsif schema.schema_uri
84
- schema.schema_uri.to_s
85
- else
86
- nil
87
- end
88
- end
89
- if !schema_names.any?(&:nil?) && !schema_names.empty?
90
- schema_names.sort.map { |n| 'X' + n.to_s.gsub(/[^\w]/, '_') }.join('')
91
- end
92
- end
60
+ def to_s
61
+ inspect
93
62
  end
94
63
 
95
- # a constant name of this class. this is generated from the schema module name or URI of each schema
96
- # this class represents. nil if any represented schema has no schema module name or schema URI.
64
+ # A constant name of this class. This is generated from any schema module name or URI of each schema
65
+ # this class represents, or random characters.
97
66
  #
98
67
  # this generated name is not too pretty but can be more helpful than an anonymous class, especially
99
68
  # in error messages.
100
69
  #
101
70
  # @return [String]
102
71
  def name
103
- unless instance_variable_defined?(:@in_schema_classes)
104
- const_name = schema_classes_const_name
105
- if super || !const_name || SchemaClasses.const_defined?(const_name)
106
- @in_schema_classes = false
72
+ return super if instance_variable_defined?(:@tried_to_name)
73
+ @tried_to_name = true
74
+ return super unless respond_to?(:jsi_class_schemas)
75
+ alnum = proc { |id| (id % 36**4).to_s(36).rjust(4, '0').upcase }
76
+ schema_names = jsi_class_schemas.map do |schema|
77
+ named_ancestor_schema, tokens = schema.jsi_schema_module.send(:named_ancestor_schema_tokens)
78
+ if named_ancestor_schema
79
+ [named_ancestor_schema.jsi_schema_module.name, *tokens].join('_')
80
+ elsif schema.schema_uri
81
+ schema.schema_uri.to_s
107
82
  else
108
- SchemaClasses.const_set(const_name, self)
109
- @in_schema_classes = true
83
+ [alnum[schema.jsi_root_node.__id__], *schema.jsi_ptr.tokens].join('_')
110
84
  end
111
85
  end
86
+ includes_names = jsi_class_includes.map { |m| m.name.sub(/\AJSI::Base::/, '').gsub(Util::RUBY_REJECT_NAME_RE, '_') }
87
+ if schema_names.any?
88
+ parts = schema_names.compact.sort.map { |n| 'X' + n.to_s }
89
+ parts += includes_names
90
+ const_name = Util.const_name_from_parts(parts, join: '__')
91
+ const_name += "__" + alnum[__id__] if SchemaClasses.const_defined?(const_name)
92
+ else
93
+ const_name = (['X' + alnum[__id__]] + includes_names).join('__')
94
+ end
95
+ # collisions are technically possible though vanishingly unlikely
96
+ SchemaClasses.const_set(const_name, self) unless SchemaClasses.const_defined?(const_name)
112
97
  super
113
98
  end
114
99
  end
@@ -117,37 +102,46 @@ module JSI
117
102
  #
118
103
  # this is a private api - users should look elsewhere to instantiate JSIs, in particular:
119
104
  #
120
- # - {JSI.new_schema} and {Schema::DescribesSchema#new_schema} to instantiate schemas
105
+ # - {JSI.new_schema} and {Schema::MetaSchema#new_schema} to instantiate schemas
121
106
  # - {Schema#new_jsi} to instantiate schema instances
122
107
  #
123
108
  # @api private
124
109
  # @param jsi_document [Object] the document containing the instance
125
110
  # @param jsi_ptr [JSI::Ptr] a pointer pointing to the JSI's instance in the document
126
- # @param jsi_root_node [JSI::Base] the JSI of the root of the document containing this JSI
127
111
  # @param jsi_schema_base_uri [Addressable::URI] see {SchemaSet#new_jsi} param uri
128
- # @param jsi_schema_resource_ancestors [Array<JSI::Base<JSI::Schema>>]
112
+ # @param jsi_schema_resource_ancestors [Array<JSI::Base + JSI::Schema>]
113
+ # @param jsi_root_node [JSI::Base] the JSI of the root of the document containing this JSI
129
114
  def initialize(jsi_document,
130
115
  jsi_ptr: Ptr[],
131
- jsi_root_node: nil,
116
+ jsi_indicated_schemas: ,
132
117
  jsi_schema_base_uri: nil,
133
- jsi_schema_resource_ancestors: Util::EMPTY_ARY
118
+ jsi_schema_resource_ancestors: Util::EMPTY_ARY,
119
+ jsi_schema_registry: ,
120
+ jsi_content_to_immutable: ,
121
+ jsi_root_node: nil
134
122
  )
135
- raise(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)
136
-
137
- jsi_initialize_memos
123
+ #chkbug raise(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)
138
124
 
139
125
  self.jsi_document = jsi_document
140
126
  self.jsi_ptr = jsi_ptr
127
+ self.jsi_indicated_schemas = jsi_indicated_schemas
128
+ self.jsi_schema_base_uri = jsi_schema_base_uri
129
+ self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors
130
+ self.jsi_schema_registry = jsi_schema_registry
131
+ @jsi_content_to_immutable = jsi_content_to_immutable
141
132
  if @jsi_ptr.root?
142
- raise(Bug, "jsi_root_node specified for root JSI") if jsi_root_node
133
+ #chkbug raise(Bug, "jsi_root_node specified for root JSI") if jsi_root_node
143
134
  @jsi_root_node = self
144
135
  else
145
- raise(Bug, "jsi_root_node is not JSI::Base") if !jsi_root_node.is_a?(JSI::Base)
146
- raise(Bug, "jsi_root_node ptr is not root") if !jsi_root_node.jsi_ptr.root?
136
+ #chkbug raise(Bug, "jsi_root_node is not JSI::Base") if !jsi_root_node.is_a?(JSI::Base)
137
+ #chkbug raise(Bug, "jsi_root_node ptr is not root") if !jsi_root_node.jsi_ptr.root?
147
138
  @jsi_root_node = jsi_root_node
148
139
  end
149
- self.jsi_schema_base_uri = jsi_schema_base_uri
150
- self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors
140
+
141
+ jsi_memomaps_initialize
142
+ jsi_mutability_initialize
143
+
144
+ super()
151
145
 
152
146
  if jsi_instance.is_a?(JSI::Base)
153
147
  raise(TypeError, "a JSI::Base instance must not be another JSI::Base. received: #{jsi_instance.pretty_inspect.chomp}")
@@ -155,7 +149,9 @@ module JSI
155
149
  end
156
150
 
157
151
  # @!method jsi_schemas
158
- # the set of schemas which describe this instance
152
+ # The set of schemas that describe this instance.
153
+ # These are the applicator schemas that apply to this instance, the result of inplace application
154
+ # of our {#jsi_indicated_schemas}.
159
155
  # @return [JSI::SchemaSet]
160
156
  # note: defined on subclasses by JSI::SchemaClasses.class_for_schemas
161
157
 
@@ -167,38 +163,66 @@ module JSI
167
163
  # @return [JSI::Ptr]
168
164
  attr_reader :jsi_ptr
169
165
 
166
+ # Comes from the param `to_immutable` of {SchemaSet#new_jsi} (or other `new_jsi` /
167
+ # `new_schema` / `new_schema_module` method).
168
+ # Immutable JSIs use this when instantiating a modified copy so its instance is also immutable.
169
+ # @return [#call, nil]
170
+ attr_reader(:jsi_content_to_immutable)
171
+
170
172
  # the JSI at the root of this JSI's document
171
173
  # @return [JSI::Base]
172
174
  attr_reader :jsi_root_node
173
175
 
174
176
  # the content of this node in our {#jsi_document} at our {#jsi_ptr}. the same as {#jsi_instance}.
175
177
  def jsi_node_content
176
- content = jsi_ptr.evaluate(jsi_document)
177
- content
178
+ # stub method for doc, overridden by Mutable/Immutable
178
179
  end
179
180
 
180
- # the JSON schema instance this JSI represents - the underlying JSON data used to instantiate this JSI
181
- alias_method :jsi_instance, :jsi_node_content
181
+ # The JSON schema instance this JSI represents - the underlying JSON data used to instantiate this JSI.
182
+ # The same as {#jsi_node_content} - 'node content' is usually preferable terminology, to avoid
183
+ # ambiguity in the heavily overloaded term 'instance'.
184
+ def jsi_instance
185
+ jsi_node_content
186
+ end
182
187
 
183
- # yields a JSI of each node at or below this one in this JSI's document.
188
+ # the schemas indicated as describing this instance, prior to inplace application.
189
+ #
190
+ # this is different from {#jsi_schemas}, which are the inplace applicator schemas
191
+ # which describe this instance. for most purposes, `#jsi_schemas` is more relevant.
192
+ #
193
+ # `jsi_indicated_schemas` does not include inplace applicator schemas, such as the
194
+ # subschemas of `allOf`, whereas `#jsi_schemas` does.
184
195
  #
185
- # returns an Enumerator if no block is given.
196
+ # this does include indicated schemas which do not apply themselves, such as `$ref`
197
+ # schemas (on json schema drafts up to 7) - these are not included on `#jsi_schemas`.
186
198
  #
199
+ # @return [JSI::SchemaSet]
200
+ attr_reader :jsi_indicated_schemas
201
+
202
+ # yields a JSI of each node at or below this one in this JSI's document.
203
+ #
204
+ # @param propertyNames [Boolean] Whether to also yield each object property
205
+ # name (Hash key) of any descendent which is a hash/object.
206
+ # These are described by `propertyNames` subshemas of that object's schemas.
207
+ # They are not actual descendents of this node.
208
+ # See {HashNode#jsi_each_propertyName}.
187
209
  # @yield [JSI::Base] each descendent node, starting with self
188
210
  # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
189
- def jsi_each_descendent_node(&block)
190
- return to_enum(__method__) unless block
211
+ def jsi_each_descendent_node(propertyNames: false, &block)
212
+ return to_enum(__method__, propertyNames: propertyNames) unless block
191
213
 
192
214
  yield self
193
- if respond_to?(:to_hash)
194
- each_key do |k|
195
- self[k, as_jsi: true].jsi_each_descendent_node(&block)
196
- end
197
- elsif respond_to?(:to_ary)
198
- each_index do |i|
199
- self[i, as_jsi: true].jsi_each_descendent_node(&block)
215
+
216
+ if propertyNames && is_a?(HashNode)
217
+ jsi_each_propertyName do |propertyName|
218
+ propertyName.jsi_each_descendent_node(propertyNames: propertyNames, &block)
200
219
  end
201
220
  end
221
+
222
+ jsi_each_child_token do |token|
223
+ jsi_child_node(token).jsi_each_descendent_node(propertyNames: propertyNames, &block)
224
+ end
225
+
202
226
  nil
203
227
  end
204
228
 
@@ -212,21 +236,17 @@ module JSI
212
236
  # @return [JSI::Base] modified copy of self containing only the selected nodes
213
237
  def jsi_select_descendents_node_first(&block)
214
238
  jsi_modified_copy do |instance|
215
- if respond_to?(:to_hash)
239
+ if jsi_array? || jsi_hash?
216
240
  res = instance.class.new
217
- each_key do |k|
218
- v = self[k, as_jsi: true]
241
+ jsi_each_child_token do |token|
242
+ v = jsi_child_node(token)
219
243
  if yield(v)
220
- res[k] = v.jsi_select_descendents_node_first(&block).jsi_node_content
221
- end
222
- end
223
- res
224
- elsif respond_to?(:to_ary)
225
- res = instance.class.new
226
- each_index do |i|
227
- e = self[i, as_jsi: true]
228
- if yield(e)
229
- res << e.jsi_select_descendents_node_first(&block).jsi_node_content
244
+ res_v = v.jsi_select_descendents_node_first(&block).jsi_node_content
245
+ if jsi_array?
246
+ res << res_v
247
+ else
248
+ res[token] = res_v
249
+ end
230
250
  end
231
251
  end
232
252
  res
@@ -236,9 +256,6 @@ module JSI
236
256
  end
237
257
  end
238
258
 
239
- # @deprecated after v0.6
240
- alias_method :jsi_select_children_node_first, :jsi_select_descendents_node_first
241
-
242
259
  # recursively selects descendent nodes of this JSI, returning a modified copy of self containing only
243
260
  # descendent nodes for which the given block had a true-ish result.
244
261
  #
@@ -249,21 +266,17 @@ module JSI
249
266
  # @return [JSI::Base] modified copy of self containing only the selected nodes
250
267
  def jsi_select_descendents_leaf_first(&block)
251
268
  jsi_modified_copy do |instance|
252
- if respond_to?(:to_hash)
269
+ if jsi_array? || jsi_hash?
253
270
  res = instance.class.new
254
- each_key do |k|
255
- v = self[k, as_jsi: true].jsi_select_descendents_leaf_first(&block)
271
+ jsi_each_child_token do |token|
272
+ v = jsi_child_node(token).jsi_select_descendents_leaf_first(&block)
256
273
  if yield(v)
257
- res[k] = v.jsi_node_content
258
- end
259
- end
260
- res
261
- elsif respond_to?(:to_ary)
262
- res = instance.class.new
263
- each_index do |i|
264
- e = self[i, as_jsi: true].jsi_select_descendents_leaf_first(&block)
265
- if yield(e)
266
- res << e.jsi_node_content
274
+ res_v = v.jsi_node_content
275
+ if jsi_array?
276
+ res << res_v
277
+ else
278
+ res[token] = res_v
279
+ end
267
280
  end
268
281
  end
269
282
  res
@@ -273,9 +286,6 @@ module JSI
273
286
  end
274
287
  end
275
288
 
276
- # @deprecated after v0.6
277
- alias_method :jsi_select_children_leaf_first, :jsi_select_descendents_leaf_first
278
-
279
289
  # an array of JSI instances above this one in the document.
280
290
  #
281
291
  # @return [Array<JSI::Base>]
@@ -284,9 +294,9 @@ module JSI
284
294
 
285
295
  jsi_ptr.tokens.map do |token|
286
296
  parent.tap do
287
- parent = parent[token, as_jsi: true]
297
+ parent = parent.jsi_child_node(token)
288
298
  end
289
- end.reverse
299
+ end.reverse!.freeze
290
300
  end
291
301
 
292
302
  # the immediate parent of this JSI. nil if there is no parent.
@@ -305,7 +315,7 @@ module JSI
305
315
  ancestors << ancestor
306
316
 
307
317
  jsi_ptr.tokens.each do |token|
308
- ancestor = ancestor[token, as_jsi: true]
318
+ ancestor = ancestor.jsi_child_node(token)
309
319
  ancestors << ancestor
310
320
  end
311
321
  ancestors.reverse!.freeze
@@ -320,19 +330,140 @@ module JSI
320
330
  descendent
321
331
  end
322
332
 
333
+ # A shorthand alias for {#jsi_descendent_node}.
334
+ #
335
+ # Note that, though more convenient to type, using an operator whose meaning may not be intuitive
336
+ # to a reader could impair readability of code.
337
+ #
338
+ # examples:
339
+ #
340
+ # my_jsi / ['foo', 'bar']
341
+ # my_jsi / %w(foo bar)
342
+ # my_schema / JSI::Ptr['additionalProperties']
343
+ # my_schema / %w(properties foo items additionalProperties)
344
+ #
345
+ # @param (see #jsi_descendent_node)
346
+ # @return (see #jsi_descendent_node)
347
+ def /(ptr)
348
+ jsi_descendent_node(ptr)
349
+ end
350
+
351
+ # yields each token (array index or hash key) identifying a child node.
352
+ # yields nothing if this node is not complex or has no children.
353
+ #
354
+ # @yield [String, Integer] each child token
355
+ # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
356
+ def jsi_each_child_token
357
+ # note: overridden by Base::HashNode, Base::ArrayNode
358
+ return to_enum(__method__) { 0 } unless block_given?
359
+ nil
360
+ end
361
+
362
+ # Does the given token identify a child of this node?
363
+ #
364
+ # In other words, is the given token an array index or hash key of the instance?
365
+ #
366
+ # Always false if this is not a complex node.
367
+ #
368
+ # @param token [String, Integer]
369
+ # @return [Boolean]
370
+ def jsi_child_token_in_range?(token)
371
+ # note: overridden by Base::HashNode, Base::ArrayNode
372
+ false
373
+ end
374
+
375
+ # The child of the {#jsi_node_content} identified by the given token,
376
+ # or `nil` if the token does not identify an existing child.
377
+ #
378
+ # In other words, the element of the instance array at the given index,
379
+ # or the value of the instance hash/object for the given key.
380
+ #
381
+ # @return [Object, nil]
382
+ # @raise [SimpleNodeChildError] if this node is not complex (its instance is not array or hash)
383
+ def jsi_node_content_child(token)
384
+ # note: overridden by Base::HashNode, Base::ArrayNode
385
+ jsi_simple_node_child_error(token)
386
+ end
387
+
388
+ # A child JSI node, or the child of our {#jsi_instance}, identified by the given token.
389
+ # The token must identify an existing child; behavior if the child does not exist is undefined.
390
+ #
391
+ # @param token (see Base#[])
392
+ # @param as_jsi (see Base#[])
393
+ # @return [JSI::Base, Object]
394
+ def jsi_child(token, as_jsi: )
395
+ child_content = jsi_node_content_child(token)
396
+
397
+ child_indicated_schemas = @child_indicated_schemas_map[token: token, content: jsi_node_content]
398
+ child_applied_schemas = @child_applied_schemas_map[token: token, child_indicated_schemas: child_indicated_schemas, child_content: child_content]
399
+
400
+ jsi_child_as_jsi(child_content, child_applied_schemas, as_jsi) do
401
+ @child_node_map[
402
+ token: token,
403
+ child_indicated_schemas: child_indicated_schemas,
404
+ child_applied_schemas: child_applied_schemas,
405
+ includes: SchemaClasses.includes_for(child_content),
406
+ ]
407
+ end
408
+ end
409
+ private :jsi_child # internals for #[] but idk, could be public
410
+
411
+ # @param token (see Base#[])
412
+ # @return [JSI::Base]
413
+ protected def jsi_child_node(token)
414
+ jsi_child(token, as_jsi: true)
415
+ end
416
+
417
+ # A default value for a child of this node identified by the given token, if schemas describing
418
+ # that child define a default value.
419
+ #
420
+ # If no schema describes a default value for the child (or in the unusual case that multiple
421
+ # schemas define different defaults), the result is `nil`.
422
+ #
423
+ # See also the `use_default` param of {Base#[]}.
424
+ #
425
+ # @param token (see Base#[])
426
+ # @param as_jsi (see Base#[])
427
+ # @return [JSI::Base, nil]
428
+ def jsi_default_child(token, as_jsi: )
429
+ child_content = jsi_node_content_child(token)
430
+
431
+ child_indicated_schemas = @child_indicated_schemas_map[token: token, content: jsi_node_content]
432
+ child_applied_schemas = @child_applied_schemas_map[token: token, child_indicated_schemas: child_indicated_schemas, child_content: child_content]
433
+
434
+ defaults = Set.new
435
+ child_applied_schemas.each do |child_schema|
436
+ if child_schema.keyword?('default')
437
+ defaults << child_schema.jsi_node_content['default']
438
+ end
439
+ end
440
+
441
+ if defaults.size == 1
442
+ # use the default value
443
+ jsi_child_as_jsi(defaults.first, child_applied_schemas, as_jsi) do
444
+ jsi_modified_copy do |i|
445
+ i.dup.tap { |i_dup| i_dup[token] = defaults.first }
446
+ end.jsi_child_node(token)
447
+ end
448
+ else
449
+ child_content
450
+ end
451
+ end
452
+ private :jsi_default_child # internals for #[] but idk, could be public
453
+
323
454
  # subscripts to return a child value identified by the given token.
324
455
  #
325
456
  # @param token [String, Integer, Object] an array index or hash key (JSON object property name)
326
457
  # of the instance identifying the child value
327
- # @param as_jsi [:auto, true, false] whether to return the result value as a JSI. one of:
458
+ # @param as_jsi [:auto, true, false] (default is `:auto`)
459
+ # Whether to return the child as a JSI. One of:
328
460
  #
329
- # - :auto (default): by default a JSI will be returned when either:
461
+ # - `:auto`: By default a JSI will be returned when either:
330
462
  #
331
463
  # - the result is a complex value (responds to #to_ary or #to_hash)
332
464
  # - the result is a schema (including true/false schemas)
333
465
  #
334
- # a plain value is returned when no schemas are known to describe the instance, or when the value is a
335
- # simple type (anything unresponsive to #to_ary / #to_hash).
466
+ # The plain content is returned when it is a simple type.
336
467
  #
337
468
  # - true: the result value will always be returned as a JSI. the {#jsi_schemas} of the result may be
338
469
  # empty if no schemas describe the instance.
@@ -342,8 +473,9 @@ module JSI
342
473
  # is not a hash key or array index of the instance and no default value applies.
343
474
  # (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
344
475
  # unspecified behavior.)
345
- # @param use_default [true, false] whether to return a schema default value when the token is not in
346
- # range. if the token is not an array index or hash key of the instance, and one schema for the child
476
+ # @param use_default [true, false] (default is `false`)
477
+ # Whether to return a schema default value when the token refers to a child that is not in the document.
478
+ # If the token is not an array index or hash key of the instance, and one schema for the child
347
479
  # instance specifies a default value, that default is returned.
348
480
  #
349
481
  # if the result with the default value is a JSI (per the `as_jsi` param), that JSI is not a child of
@@ -354,52 +486,23 @@ module JSI
354
486
  # defaults are specified across those schemas), nil is returned.
355
487
  # (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
356
488
  # unspecified behavior.)
357
- # @return [JSI::Base, Object] the child value identified by the subscript token
358
- def [](token, as_jsi: :auto, use_default: true)
359
- if respond_to?(:to_hash)
360
- token_in_range = jsi_node_content_hash_pubsend(:key?, token)
361
- value = jsi_node_content_hash_pubsend(:[], token)
362
- elsif respond_to?(:to_ary)
363
- token_in_range = jsi_node_content_ary_pubsend(:each_index).include?(token)
364
- value = jsi_node_content_ary_pubsend(:[], token)
365
- else
366
- raise(CannotSubscriptError, "cannot subscript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
367
- end
368
-
369
- begin
370
- subinstance_schemas = jsi_subinstance_schemas_memos[token: token, instance: jsi_node_content, subinstance: value]
489
+ # @return [JSI::Base, Object, nil] the child value identified by the subscript token
490
+ def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default)
491
+ # note: overridden by Base::HashNode, Base::ArrayNode
492
+ jsi_simple_node_child_error(token)
493
+ end
371
494
 
372
- if token_in_range
373
- jsi_subinstance_as_jsi(value, subinstance_schemas, as_jsi) do
374
- jsi_subinstance_memos[
375
- token: token,
376
- subinstance_schemas: subinstance_schemas,
377
- includes: SchemaClasses.includes_for(value),
378
- ]
379
- end
380
- else
381
- if use_default
382
- defaults = Set.new
383
- subinstance_schemas.each do |subinstance_schema|
384
- if subinstance_schema.keyword?('default')
385
- defaults << subinstance_schema.jsi_node_content['default']
386
- end
387
- end
388
- end
495
+ # The default value for the param `as_jsi` of {#[]}, controlling whether a child is returned as a JSI instance.
496
+ # @return [:auto, true, false] a valid value of the `as_jsi` param of {#[]}
497
+ def jsi_child_as_jsi_default
498
+ :auto
499
+ end
389
500
 
390
- if use_default && defaults.size == 1
391
- # use the default value
392
- # we are using #dup so that we get a modified copy of self, in which we set dup[token]=default.
393
- dup.tap { |o| o[token] = defaults.first }[token, as_jsi: as_jsi]
394
- else
395
- # I kind of want to just return nil here. the preferred mechanism for
396
- # a JSI's default value should be its schema. but returning nil ignores
397
- # any value returned by Hash#default/#default_proc. there's no compelling
398
- # reason not to support both, so I'll return that.
399
- value
400
- end
401
- end
402
- end
501
+ # The default value for the param `use_default` of {#[]}, controlling whether a schema default value is
502
+ # returned when a token refers to a child that is not in the document.
503
+ # @return [true, false] a valid value of the `use_default` param of {#[]}
504
+ def jsi_child_use_default_default
505
+ false
403
506
  end
404
507
 
405
508
  # assigns the subscript of the instance identified by the given token to the given value.
@@ -408,8 +511,8 @@ module JSI
408
511
  # @param token [String, Integer, Object] token identifying the subscript to assign
409
512
  # @param value [JSI::Base, Object] the value to be assigned
410
513
  def []=(token, value)
411
- unless respond_to?(:to_hash) || respond_to?(:to_ary)
412
- raise(CannotSubscriptError, "cannot assign subscript (using token: #{token.inspect}) to instance: #{jsi_instance.pretty_inspect.chomp}")
514
+ unless jsi_array? || jsi_hash?
515
+ jsi_simple_node_child_error(token)
413
516
  end
414
517
  if value.is_a?(Base)
415
518
  self[token] = value.jsi_instance
@@ -424,6 +527,26 @@ module JSI
424
527
  Util.ensure_module_set(jsi_schemas.map(&:jsi_schema_module))
425
528
  end
426
529
 
530
+ # Is this JSI described by the given schema (or schema module)?
531
+ #
532
+ # @param schema [Schema, SchemaModule]
533
+ # @return [Boolean]
534
+ def described_by?(schema)
535
+ if schema.is_a?(Schema)
536
+ jsi_schemas.include?(schema)
537
+ elsif schema.is_a?(SchemaModule)
538
+ jsi_schema_modules.include?(schema)
539
+ else
540
+ raise(TypeError, "expected a Schema or Schema Module; got: #{schema.pretty_inspect.chomp}")
541
+ end
542
+ end
543
+
544
+ # Is this a JSI Schema?
545
+ # @return [Boolean]
546
+ def jsi_is_schema?
547
+ false
548
+ end
549
+
427
550
  # yields the content of this JSI's instance. the block must result in
428
551
  # a modified copy of the yielded instance (not modified in place, which would alter this JSI
429
552
  # as well) which will be used to instantiate and return a new JSI with the modified content.
@@ -435,30 +558,58 @@ module JSI
435
558
  # in a nondestructively modified copy of this.
436
559
  # @return [JSI::Base subclass] the modified copy of self
437
560
  def jsi_modified_copy(&block)
438
- if @jsi_ptr.root?
439
561
  modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
440
- jsi_schemas.new_jsi(modified_document,
441
- uri: jsi_schema_base_uri,
562
+ modified_jsi_root_node = @jsi_root_node.jsi_indicated_schemas.new_jsi(modified_document,
563
+ uri: @jsi_root_node.jsi_schema_base_uri,
564
+ register: false, # default is already false but this is a place to be explicit
565
+ schema_registry: jsi_schema_registry,
566
+ mutable: jsi_mutable?,
567
+ to_immutable: jsi_content_to_immutable,
442
568
  )
443
- else
444
- modified_jsi_root_node = @jsi_root_node.jsi_modified_copy do |root|
445
- @jsi_ptr.modified_document_copy(root, &block)
446
- end
447
569
  modified_jsi_root_node.jsi_descendent_node(@jsi_ptr)
448
- end
570
+ end
571
+
572
+ # Is the instance an array?
573
+ #
574
+ # An array is typically an instance of the Array class but may be an object that supports
575
+ # [implicit conversion](https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html)
576
+ # with a `#to_ary` method.
577
+ #
578
+ # @return [Boolean]
579
+ def jsi_array?
580
+ # note: overridden by Base::ArrayNode
581
+ false
582
+ end
583
+
584
+ # Is the instance a ruby Hash (JSON object)?
585
+ #
586
+ # This is typically an instance of the Hash class but may be an object that supports
587
+ # [implicit conversion](https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html)
588
+ # with a `#to_hash` method.
589
+ #
590
+ # @return [Boolean]
591
+ def jsi_hash?
592
+ # note: overridden by Base::HashNode
593
+ false
594
+ end
595
+
596
+ # Is this JSI mutable?
597
+ # @return [Boolean]
598
+ def jsi_mutable?
599
+ # note: overridden by Base::Mutable / Immutable
449
600
  end
450
601
 
451
602
  # validates this JSI's instance against its schemas
452
603
  #
453
604
  # @return [JSI::Validation::FullResult]
454
605
  def jsi_validate
455
- jsi_schemas.instance_validate(self)
606
+ jsi_indicated_schemas.instance_validate(self)
456
607
  end
457
608
 
458
609
  # whether this JSI's instance is valid against all of its schemas
459
610
  # @return [Boolean]
460
611
  def jsi_valid?
461
- jsi_schemas.instance_valid?(self)
612
+ jsi_indicated_schemas.instance_valid?(self)
462
613
  end
463
614
 
464
615
  # queries this JSI using the [JMESPath Ruby](https://rubygems.org/gems/jmespath) gem.
@@ -485,21 +636,21 @@ module JSI
485
636
  # a string representing this JSI, indicating any named schemas and inspecting its instance
486
637
  # @return [String]
487
638
  def inspect
488
- "\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
639
+ -"\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
489
640
  end
490
641
 
491
- alias_method :to_s, :inspect
642
+ def to_s
643
+ inspect
644
+ end
492
645
 
493
646
  # pretty-prints a representation of this JSI to the given printer
494
647
  # @return [void]
495
648
  def pretty_print(q)
496
649
  q.text '#<'
497
650
  q.text jsi_object_group_text.join(' ')
498
- q.group_sub {
499
- q.nest(2) {
651
+ q.group(2) {
500
652
  q.breakable ' '
501
653
  q.pp jsi_instance
502
- }
503
654
  }
504
655
  q.breakable ''
505
656
  q.text '>'
@@ -512,82 +663,115 @@ module JSI
512
663
  if schema_names.empty?
513
664
  class_txt = "JSI"
514
665
  else
515
- class_txt = "JSI (#{schema_names.join(', ')})"
666
+ class_txt = -"JSI (#{schema_names.join(', ')})"
516
667
  end
517
668
 
518
669
  if (is_a?(ArrayNode) || is_a?(HashNode)) && ![Array, Hash].include?(jsi_node_content.class)
519
670
  if jsi_node_content.respond_to?(:jsi_object_group_text)
520
671
  content_txt = jsi_node_content.jsi_object_group_text
521
672
  else
522
- content_txt = [jsi_node_content.class.to_s]
673
+ content_txt = jsi_node_content.class.to_s
523
674
  end
524
675
  else
525
- content_txt = []
676
+ content_txt = nil
526
677
  end
527
678
 
528
679
  [
529
680
  class_txt,
530
- is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
681
+ is_a?(Schema::MetaSchema) ? "Meta-Schema" : is_a?(Schema) ? "Schema" : nil,
531
682
  *content_txt,
532
- ].compact
683
+ ].compact.freeze
533
684
  end
534
685
 
535
- # a jsonifiable representation of the instance
536
- # @return [Object]
537
- def as_json(*opt)
538
- Util.as_json(jsi_instance, *opt)
686
+ # A structure coerced to JSONifiable types from the instance content.
687
+ # Calls {Util.as_json} with the instance and any given options.
688
+ def as_json(options = {})
689
+ Util.as_json(jsi_instance, **options)
539
690
  end
540
691
 
541
- # an opaque fingerprint of this JSI for {Util::FingerprintHash}.
692
+ # A JSON encoded string of the instance content.
693
+ # Calls {Util.to_json} with the instance and any given options.
694
+ # @return [String]
695
+ def to_json(options = {})
696
+ Util.to_json(jsi_instance, **options)
697
+ end
698
+
699
+ # see {Util::Private::FingerprintHash}
700
+ # @api private
542
701
  def jsi_fingerprint
543
702
  {
544
- class: jsi_class,
703
+ class: JSI::Base,
704
+ jsi_schemas: jsi_schemas,
545
705
  jsi_document: jsi_document,
546
706
  jsi_ptr: jsi_ptr,
547
707
  # for instances in documents with schemas:
548
708
  jsi_resource_ancestor_uri: jsi_resource_ancestor_uri,
549
- # only defined for JSI::Schema instances:
550
- jsi_schema_instance_modules: is_a?(Schema) ? jsi_schema_instance_modules : nil,
551
- }
709
+ # different registries mean references may resolve to different resources so must not be equal
710
+ jsi_schema_registry: jsi_schema_registry,
711
+ }.freeze
552
712
  end
553
- include Util::FingerprintHash
554
713
 
555
714
  private
556
715
 
557
- def jsi_subinstance_schemas_memos
558
- jsi_memomap(:subinstance_schemas, key_by: -> (i) { i[:token] }) do |token: , instance: , subinstance: |
559
- jsi_schemas.child_applicator_schemas(token, instance).inplace_applicator_schemas(subinstance)
560
- end
716
+ def jsi_memomaps_initialize
717
+ @child_indicated_schemas_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_indicated_schemas_compute))
718
+ @child_applied_schemas_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_applied_schemas_compute))
719
+ @child_node_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_node_compute))
561
720
  end
562
721
 
563
- def jsi_subinstance_memos
564
- jsi_memomap(:subinstance, key_by: -> (i) { i[:token] }) do |token: , subinstance_schemas: , includes: |
565
- jsi_class = JSI::SchemaClasses.class_for_schemas(subinstance_schemas, includes: includes)
722
+ def jsi_indicated_schemas=(jsi_indicated_schemas)
723
+ #chkbug raise(Bug) unless jsi_indicated_schemas.is_a?(SchemaSet)
724
+ @jsi_indicated_schemas = jsi_indicated_schemas
725
+ end
726
+
727
+ def jsi_child_node_compute(token: , child_indicated_schemas: , child_applied_schemas: , includes: )
728
+ jsi_class = JSI::SchemaClasses.class_for_schemas(child_applied_schemas,
729
+ includes: includes,
730
+ mutable: jsi_mutable?,
731
+ )
566
732
  jsi_class.new(@jsi_document,
567
733
  jsi_ptr: @jsi_ptr[token],
568
- jsi_root_node: @jsi_root_node,
734
+ jsi_indicated_schemas: child_indicated_schemas,
569
735
  jsi_schema_base_uri: jsi_resource_ancestor_uri,
570
736
  jsi_schema_resource_ancestors: is_a?(Schema) ? jsi_subschema_resource_ancestors : jsi_schema_resource_ancestors,
737
+ jsi_schema_registry: jsi_schema_registry,
738
+ jsi_content_to_immutable: @jsi_content_to_immutable,
739
+ jsi_root_node: @jsi_root_node,
571
740
  )
572
- end
573
741
  end
574
742
 
575
- def jsi_subinstance_as_jsi(value, subinstance_schemas, as_jsi)
743
+ def jsi_child_indicated_schemas_compute(token: , content: )
744
+ jsi_schemas.child_applicator_schemas(token, content)
745
+ end
746
+
747
+ def jsi_child_applied_schemas_compute(token: , child_indicated_schemas: , child_content: )
748
+ child_indicated_schemas.inplace_applicator_schemas(child_content)
749
+ end
750
+
751
+ def jsi_child_as_jsi(child_content, child_schemas, as_jsi)
576
752
  if [true, false].include?(as_jsi)
577
- value_as_jsi = as_jsi
753
+ child_as_jsi = as_jsi
578
754
  elsif as_jsi == :auto
579
- complex_value = value.respond_to?(:to_hash) || value.respond_to?(:to_ary)
580
- schema_value = subinstance_schemas.any?(&:describes_schema?)
581
- value_as_jsi = complex_value || schema_value
755
+ child_is_complex = child_content.respond_to?(:to_hash) || child_content.respond_to?(:to_ary)
756
+ child_is_schema = child_schemas.any?(&:describes_schema?)
757
+ child_as_jsi = child_is_complex || child_is_schema
582
758
  else
583
759
  raise(ArgumentError, "as_jsi must be one of: :auto, true, false")
584
760
  end
585
761
 
586
- if value_as_jsi
762
+ if child_as_jsi
587
763
  yield
588
764
  else
589
- value
765
+ child_content
590
766
  end
591
767
  end
768
+
769
+ def jsi_simple_node_child_error(token)
770
+ raise(SimpleNodeChildError, [
771
+ "cannot access a child of this JSI node because this node is not complex",
772
+ "using token: #{token.inspect}",
773
+ "instance: #{jsi_instance.pretty_inspect.chomp}",
774
+ ].join("\n"))
775
+ end
592
776
  end
593
777
  end