jsi 0.7.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +29 -20
  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 +408 -198
  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. data/readme.rb +1 -1
  56. metadata +19 -5
  57. data/lib/jsi/metaschema.rb +0 -6
  58. 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