jsi 0.6.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -1
  3. data/CHANGELOG.md +33 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +29 -23
  6. data/jsi.gemspec +29 -0
  7. data/lib/jsi/base/mutability.rb +44 -0
  8. data/lib/jsi/base/node.rb +348 -0
  9. data/lib/jsi/base.rb +497 -339
  10. data/lib/jsi/jsi_coder.rb +19 -17
  11. data/lib/jsi/metaschema_node/bootstrap_schema.rb +61 -26
  12. data/lib/jsi/metaschema_node.rb +161 -133
  13. data/lib/jsi/ptr.rb +80 -47
  14. data/lib/jsi/schema/application/child_application/contains.rb +11 -2
  15. data/lib/jsi/schema/application/child_application/draft04.rb +0 -1
  16. data/lib/jsi/schema/application/child_application/draft06.rb +0 -1
  17. data/lib/jsi/schema/application/child_application/draft07.rb +0 -1
  18. data/lib/jsi/schema/application/child_application/items.rb +3 -3
  19. data/lib/jsi/schema/application/child_application/properties.rb +3 -3
  20. data/lib/jsi/schema/application/child_application.rb +0 -27
  21. data/lib/jsi/schema/application/inplace_application/dependencies.rb +1 -1
  22. data/lib/jsi/schema/application/inplace_application/draft04.rb +0 -1
  23. data/lib/jsi/schema/application/inplace_application/draft06.rb +0 -1
  24. data/lib/jsi/schema/application/inplace_application/draft07.rb +0 -1
  25. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +3 -3
  26. data/lib/jsi/schema/application/inplace_application/ref.rb +2 -2
  27. data/lib/jsi/schema/application/inplace_application/someof.rb +26 -11
  28. data/lib/jsi/schema/application/inplace_application.rb +0 -32
  29. data/lib/jsi/schema/draft04.rb +0 -1
  30. data/lib/jsi/schema/draft06.rb +0 -1
  31. data/lib/jsi/schema/draft07.rb +0 -1
  32. data/lib/jsi/schema/ref.rb +46 -19
  33. data/lib/jsi/schema/schema_ancestor_node.rb +69 -66
  34. data/lib/jsi/schema/validation/array.rb +3 -3
  35. data/lib/jsi/schema/validation/const.rb +1 -1
  36. data/lib/jsi/schema/validation/contains.rb +2 -2
  37. data/lib/jsi/schema/validation/dependencies.rb +1 -1
  38. data/lib/jsi/schema/validation/draft04/minmax.rb +8 -6
  39. data/lib/jsi/schema/validation/draft04.rb +0 -2
  40. data/lib/jsi/schema/validation/draft06.rb +0 -2
  41. data/lib/jsi/schema/validation/draft07.rb +0 -2
  42. data/lib/jsi/schema/validation/enum.rb +1 -1
  43. data/lib/jsi/schema/validation/ifthenelse.rb +5 -5
  44. data/lib/jsi/schema/validation/items.rb +7 -7
  45. data/lib/jsi/schema/validation/not.rb +1 -1
  46. data/lib/jsi/schema/validation/numeric.rb +5 -5
  47. data/lib/jsi/schema/validation/object.rb +2 -2
  48. data/lib/jsi/schema/validation/pattern.rb +2 -2
  49. data/lib/jsi/schema/validation/properties.rb +7 -7
  50. data/lib/jsi/schema/validation/property_names.rb +1 -1
  51. data/lib/jsi/schema/validation/ref.rb +2 -2
  52. data/lib/jsi/schema/validation/required.rb +1 -1
  53. data/lib/jsi/schema/validation/someof.rb +3 -3
  54. data/lib/jsi/schema/validation/string.rb +2 -2
  55. data/lib/jsi/schema/validation/type.rb +1 -1
  56. data/lib/jsi/schema/validation.rb +1 -3
  57. data/lib/jsi/schema.rb +443 -226
  58. data/lib/jsi/schema_classes.rb +241 -147
  59. data/lib/jsi/schema_registry.rb +78 -19
  60. data/lib/jsi/schema_set.rb +114 -28
  61. data/lib/jsi/simple_wrap.rb +18 -4
  62. data/lib/jsi/util/private/attr_struct.rb +141 -0
  63. data/lib/jsi/util/private/memo_map.rb +75 -0
  64. data/lib/jsi/util/private.rb +185 -0
  65. data/lib/jsi/{typelike_modules.rb → util/typelike.rb} +79 -105
  66. data/lib/jsi/util.rb +157 -153
  67. data/lib/jsi/validation/error.rb +4 -0
  68. data/lib/jsi/validation/result.rb +18 -32
  69. data/lib/jsi/version.rb +1 -1
  70. data/lib/jsi.rb +65 -39
  71. data/lib/schemas/json-schema.org/draft-04/schema.rb +160 -3
  72. data/lib/schemas/json-schema.org/draft-06/schema.rb +162 -3
  73. data/lib/schemas/json-schema.org/draft-07/schema.rb +189 -3
  74. metadata +27 -11
  75. data/lib/jsi/metaschema.rb +0 -7
  76. data/lib/jsi/pathed_node.rb +0 -116
  77. data/lib/jsi/schema/validation/core.rb +0 -39
  78. data/lib/jsi/util/attr_struct.rb +0 -106
data/lib/jsi/base.rb CHANGED
@@ -1,47 +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
- include PathedNode
18
- include Schema::SchemaAncestorNode
19
- include Util::Memoize
18
+ autoload :ArrayNode, 'jsi/base/node'
19
+ autoload :HashNode, 'jsi/base/node'
20
+ autoload :StringNode, 'jsi/base/node'
21
+ autoload(:Mutable, 'jsi/base/mutability')
22
+ autoload(:Immutable, 'jsi/base/mutability')
20
23
 
21
- # not every JSI::Base is necessarily an Enumerable, but it's better to include Enumerable on
22
- # the class than to conditionally extend the instance.
23
- include Enumerable
24
+ include Schema::SchemaAncestorNode
24
25
 
25
- # an exception raised when #[] is invoked on an instance which is not an array or hash
26
- 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
27
29
  end
28
30
 
29
31
  class << self
30
- # @private @deprecated
31
- def new_jsi(instance, **kw, &b)
32
- new(instance, **kw, &b)
33
- end
34
-
35
- # @private
36
- # is the constant JSI::SchemaClasses::<self.schema_classes_const_name> defined?
37
- # (if so, we will prefer to use something more human-readable than that ugly mess.)
38
- def in_schema_classes
39
- # #name sets @in_schema_classes
40
- name
41
- @in_schema_classes
42
- end
43
-
44
- # 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
45
33
  # and/or schema URI of each schema the class represents.
46
34
  # @return [String]
47
35
  def inspect
@@ -49,11 +37,11 @@ module JSI
49
37
  super
50
38
  else
51
39
  schema_names = jsi_class_schemas.map do |schema|
52
- mod = schema.jsi_schema_module
53
- if mod.name && schema.schema_uri
54
- "#{mod.name} (#{schema.schema_uri})"
55
- elsif mod.name
56
- 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
57
45
  elsif schema.schema_uri
58
46
  schema.schema_uri.to_s
59
47
  else
@@ -61,141 +49,109 @@ module JSI
61
49
  end
62
50
  end
63
51
 
64
- if name && !in_schema_classes
65
- if jsi_class_schemas.empty?
66
- "#{name} (0 schemas)"
67
- else
68
- "#{name} (#{schema_names.join(', ')})"
69
- end
52
+ if schema_names.empty?
53
+ "(JSI Schema Class for 0 schemas#{jsi_class_includes.map { |n| " + #{n}" }})"
70
54
  else
71
- if schema_names.empty?
72
- "(JSI Schema Class for 0 schemas)"
73
- else
74
- "(JSI Schema Class: #{schema_names.join(', ')})"
75
- end
55
+ -"(JSI Schema Class: #{(schema_names + jsi_class_includes.map(&:name)).join(' + ')})"
76
56
  end
77
57
  end
78
58
  end
79
59
 
80
- alias_method :to_s, :inspect
81
-
82
- # @private
83
- # see {.name}
84
- def schema_classes_const_name
85
- if respond_to?(:jsi_class_schemas)
86
- schema_names = jsi_class_schemas.map do |schema|
87
- if schema.jsi_schema_module.name
88
- schema.jsi_schema_module.name
89
- elsif schema.schema_uri
90
- schema.schema_uri.to_s
91
- else
92
- nil
93
- end
94
- end
95
- if !schema_names.any?(&:nil?) && !schema_names.empty?
96
- schema_names.sort.map { |n| 'X' + n.to_s.gsub(/[^\w]/, '_') }.join('')
97
- end
98
- end
60
+ def to_s
61
+ inspect
99
62
  end
100
63
 
101
- # a constant name of this class. this is generated from the schema module name or URI of each schema
102
- # 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.
103
66
  #
104
67
  # this generated name is not too pretty but can be more helpful than an anonymous class, especially
105
68
  # in error messages.
106
69
  #
107
70
  # @return [String]
108
71
  def name
109
- unless instance_variable_defined?(:@in_schema_classes)
110
- const_name = schema_classes_const_name
111
- if super || !const_name || SchemaClasses.const_defined?(const_name)
112
- @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
113
82
  else
114
- SchemaClasses.const_set(const_name, self)
115
- @in_schema_classes = true
83
+ [alnum[schema.jsi_root_node.__id__], *schema.jsi_ptr.tokens].join('_')
116
84
  end
117
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)
118
97
  super
119
98
  end
120
99
  end
121
100
 
122
- # NOINSTANCE is a magic value passed to #initialize when instantiating a JSI
123
- # from a document and pointer.
101
+ # initializes a JSI whose instance is in the given document at the given pointer.
124
102
  #
125
- # @private
126
- NOINSTANCE = Object.new
127
- [:inspect, :to_s].each(&(-> (s, m) { NOINSTANCE.define_singleton_method(m) { s } }.curry.("#{JSI::Base}::NOINSTANCE")))
128
- NOINSTANCE.freeze
129
-
130
- # initializes this JSI from the given instance - instance is most commonly
131
- # a parsed JSON document consisting of Hash, Array, or sometimes a basic
132
- # type, but this is in no way enforced and a JSI may wrap any object.
133
- #
134
- # @param instance [Object] the JSON Schema instance to be represented as a JSI
135
- # @param jsi_document [Object] for internal use. the instance may be specified as a
136
- # node in the `jsi_document` param, pointed to by `jsi_ptr`. the param `instance`
137
- # MUST be `NOINSTANCE` to use the jsi_document + jsi_ptr form. `jsi_document` MUST
138
- # NOT be passed if `instance` is anything other than `NOINSTANCE`.
139
- # @param jsi_ptr [JSI::Ptr] for internal use. a pointer specifying
140
- # the path of this instance in the `jsi_document` param. `jsi_ptr` must be passed
141
- # iff `jsi_document` is passed, i.e. when `instance` is `NOINSTANCE`
142
- # @param jsi_root_node [JSI::Base] for internal use, specifies the JSI at the root of the document
103
+ # this is a private api - users should look elsewhere to instantiate JSIs, in particular:
104
+ #
105
+ # - {JSI.new_schema} and {Schema::MetaSchema#new_schema} to instantiate schemas
106
+ # - {Schema#new_jsi} to instantiate schema instances
107
+ #
108
+ # @api private
109
+ # @param jsi_document [Object] the document containing the instance
110
+ # @param jsi_ptr [JSI::Ptr] a pointer pointing to the JSI's instance in the document
143
111
  # @param jsi_schema_base_uri [Addressable::URI] see {SchemaSet#new_jsi} param uri
144
- # @param jsi_schema_resource_ancestors [Array<JSI::Base>]
145
- def initialize(instance,
146
- jsi_document: nil,
147
- jsi_ptr: nil,
148
- jsi_root_node: nil,
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
114
+ def initialize(jsi_document,
115
+ jsi_ptr: Ptr[],
116
+ jsi_indicated_schemas: ,
149
117
  jsi_schema_base_uri: nil,
150
- jsi_schema_resource_ancestors: []
118
+ jsi_schema_resource_ancestors: Util::EMPTY_ARY,
119
+ jsi_schema_registry: ,
120
+ jsi_content_to_immutable: ,
121
+ jsi_root_node: nil
151
122
  )
152
- unless respond_to?(:jsi_schemas)
153
- raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #jsi_schemas. it is recommended to instantiate JSIs from a schema using JSI::Schema#new_jsi.")
154
- end
123
+ #chkbug raise(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)
155
124
 
156
- if instance.is_a?(JSI::Schema)
157
- raise(TypeError, "assigning a schema to a #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
158
- elsif instance.is_a?(JSI::Base)
159
- raise(TypeError, "assigning another JSI::Base instance to a #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
160
- end
161
-
162
- jsi_initialize_memos
163
-
164
- if instance == NOINSTANCE
165
- self.jsi_document = jsi_document
166
- self.jsi_ptr = jsi_ptr
167
- if @jsi_ptr.root?
168
- raise(Bug, "jsi_root_node cannot be specified for root JSI") if jsi_root_node
169
- @jsi_root_node = self
170
- else
171
- if !jsi_root_node.is_a?(JSI::Base)
172
- raise(TypeError, "jsi_root_node must be a JSI::Base; got: #{jsi_root_node.inspect}")
173
- end
174
- if !jsi_root_node.jsi_ptr.root?
175
- raise(Bug, "jsi_root_node ptr #{jsi_root_node.jsi_ptr.inspect} is not root")
176
- end
177
- @jsi_root_node = jsi_root_node
178
- end
179
- else
180
- raise(Bug, 'incorrect usage') if jsi_document || jsi_ptr || jsi_root_node
181
- @jsi_document = instance
182
- @jsi_ptr = Ptr[]
125
+ self.jsi_document = jsi_document
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
132
+ if @jsi_ptr.root?
133
+ #chkbug raise(Bug, "jsi_root_node specified for root JSI") if jsi_root_node
183
134
  @jsi_root_node = self
135
+ else
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?
138
+ @jsi_root_node = jsi_root_node
184
139
  end
185
140
 
186
- self.jsi_schema_base_uri = jsi_schema_base_uri
187
- self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors
141
+ jsi_memomaps_initialize
142
+ jsi_mutability_initialize
188
143
 
189
- if self.jsi_instance.respond_to?(:to_hash)
190
- extend PathedHashNode
191
- end
192
- if self.jsi_instance.respond_to?(:to_ary)
193
- extend PathedArrayNode
144
+ super()
145
+
146
+ if jsi_instance.is_a?(JSI::Base)
147
+ raise(TypeError, "a JSI::Base instance must not be another JSI::Base. received: #{jsi_instance.pretty_inspect.chomp}")
194
148
  end
195
149
  end
196
150
 
197
151
  # @!method jsi_schemas
198
- # 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}.
199
155
  # @return [JSI::SchemaSet]
200
156
  # note: defined on subclasses by JSI::SchemaClasses.class_for_schemas
201
157
 
@@ -207,66 +163,90 @@ module JSI
207
163
  # @return [JSI::Ptr]
208
164
  attr_reader :jsi_ptr
209
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
+
210
172
  # the JSI at the root of this JSI's document
211
173
  # @return [JSI::Base]
212
174
  attr_reader :jsi_root_node
213
175
 
214
- # the JSON schema instance this JSI represents - the underlying JSON data used to instantiate this JSI
215
- alias_method :jsi_instance, :jsi_node_content
176
+ # the content of this node in our {#jsi_document} at our {#jsi_ptr}. the same as {#jsi_instance}.
177
+ def jsi_node_content
178
+ # stub method for doc, overridden by Mutable/Immutable
179
+ end
216
180
 
217
- # each is overridden by PathedHashNode or PathedArrayNode when appropriate. the base #each
218
- # is not actually implemented, along with all the methods of Enumerable.
219
- def each(*_)
220
- raise NoMethodError, "Enumerable methods and #each not implemented for instance that is not like a hash or array: #{jsi_instance.pretty_inspect.chomp}"
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
221
186
  end
222
187
 
223
- # 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.
224
189
  #
225
- # returns an Enumerator if no block is given.
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.
226
192
  #
227
- # @yield [JSI::Base] each node in the document, starting with self
193
+ # `jsi_indicated_schemas` does not include inplace applicator schemas, such as the
194
+ # subschemas of `allOf`, whereas `#jsi_schemas` does.
195
+ #
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`.
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}.
209
+ # @yield [JSI::Base] each descendent node, starting with self
228
210
  # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
229
- def jsi_each_child_node(&block)
230
- return to_enum(__method__) unless block
211
+ def jsi_each_descendent_node(propertyNames: false, &block)
212
+ return to_enum(__method__, propertyNames: propertyNames) unless block
231
213
 
232
214
  yield self
233
- if respond_to?(:to_hash)
234
- each_key do |k|
235
- self[k, as_jsi: true].jsi_each_child_node(&block)
236
- end
237
- elsif respond_to?(:to_ary)
238
- each_index do |i|
239
- self[i, as_jsi: true].jsi_each_child_node(&block)
215
+
216
+ if propertyNames && is_a?(HashNode)
217
+ jsi_each_propertyName do |propertyName|
218
+ propertyName.jsi_each_descendent_node(propertyNames: propertyNames, &block)
240
219
  end
241
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
+
242
226
  nil
243
227
  end
244
228
 
245
- # recursively selects child nodes of this JSI, returning a modified copy of self containing only
246
- # child nodes for which the given block had a true-ish result.
229
+ # recursively selects descendent nodes of this JSI, returning a modified copy of self containing only
230
+ # descendent nodes for which the given block had a true-ish result.
247
231
  #
248
232
  # this method yields a node before recursively descending to its child nodes, so leaf nodes are yielded
249
- # last, after their parents. if a node is not selected, its children are never recursed.
233
+ # last, after their parents. if a node is not selected, its descendents are never recursed.
250
234
  #
251
- # @yield [JSI::Base] each child node below self
235
+ # @yield [JSI::Base] each descendent node below self
252
236
  # @return [JSI::Base] modified copy of self containing only the selected nodes
253
- def jsi_select_children_node_first(&block)
237
+ def jsi_select_descendents_node_first(&block)
254
238
  jsi_modified_copy do |instance|
255
- if respond_to?(:to_hash)
239
+ if jsi_array? || jsi_hash?
256
240
  res = instance.class.new
257
- each_key do |k|
258
- v = self[k, as_jsi: true]
241
+ jsi_each_child_token do |token|
242
+ v = jsi_child_node(token)
259
243
  if yield(v)
260
- res[k] = v.jsi_select_children_node_first(&block).jsi_node_content
261
- end
262
- end
263
- res
264
- elsif respond_to?(:to_ary)
265
- res = instance.class.new
266
- each_index do |i|
267
- e = self[i, as_jsi: true]
268
- if yield(e)
269
- res << e.jsi_select_children_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
270
250
  end
271
251
  end
272
252
  res
@@ -276,31 +256,27 @@ module JSI
276
256
  end
277
257
  end
278
258
 
279
- # recursively selects child nodes of this JSI, returning a modified copy of self containing only
280
- # child nodes for which the given block had a true-ish result.
259
+ # recursively selects descendent nodes of this JSI, returning a modified copy of self containing only
260
+ # descendent nodes for which the given block had a true-ish result.
281
261
  #
282
262
  # this method recursively descends child nodes before yielding each node, so leaf nodes are yielded
283
263
  # before their parents.
284
264
  #
285
- # @yield [JSI::Base] each child node below self
265
+ # @yield [JSI::Base] each descendent node below self
286
266
  # @return [JSI::Base] modified copy of self containing only the selected nodes
287
- def jsi_select_children_leaf_first(&block)
267
+ def jsi_select_descendents_leaf_first(&block)
288
268
  jsi_modified_copy do |instance|
289
- if respond_to?(:to_hash)
269
+ if jsi_array? || jsi_hash?
290
270
  res = instance.class.new
291
- each_key do |k|
292
- v = self[k, as_jsi: true].jsi_select_children_leaf_first(&block)
271
+ jsi_each_child_token do |token|
272
+ v = jsi_child_node(token).jsi_select_descendents_leaf_first(&block)
293
273
  if yield(v)
294
- res[k] = v.jsi_node_content
295
- end
296
- end
297
- res
298
- elsif respond_to?(:to_ary)
299
- res = instance.class.new
300
- each_index do |i|
301
- e = self[i, as_jsi: true].jsi_select_children_leaf_first(&block)
302
- if yield(e)
303
- 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
304
280
  end
305
281
  end
306
282
  res
@@ -318,42 +294,188 @@ module JSI
318
294
 
319
295
  jsi_ptr.tokens.map do |token|
320
296
  parent.tap do
321
- parent = parent[token, as_jsi: true]
297
+ parent = parent.jsi_child_node(token)
322
298
  end
323
- end.reverse
299
+ end.reverse!.freeze
324
300
  end
325
301
 
326
302
  # the immediate parent of this JSI. nil if there is no parent.
327
303
  #
328
304
  # @return [JSI::Base, nil]
329
305
  def jsi_parent_node
330
- jsi_parent_nodes.first
306
+ jsi_ptr.root? ? nil : jsi_root_node.jsi_descendent_node(jsi_ptr.parent)
307
+ end
308
+
309
+ # ancestor JSI instances from this node up to the root. this node itself is always its own first ancestor.
310
+ #
311
+ # @return [Array<JSI::Base>]
312
+ def jsi_ancestor_nodes
313
+ ancestors = []
314
+ ancestor = jsi_root_node
315
+ ancestors << ancestor
316
+
317
+ jsi_ptr.tokens.each do |token|
318
+ ancestor = ancestor.jsi_child_node(token)
319
+ ancestors << ancestor
320
+ end
321
+ ancestors.reverse!.freeze
322
+ end
323
+
324
+ # the descendent node at the given pointer
325
+ #
326
+ # @param ptr [JSI::Ptr, #to_ary]
327
+ # @return [JSI::Base]
328
+ def jsi_descendent_node(ptr)
329
+ descendent = Ptr.ary_ptr(ptr).evaluate(self, as_jsi: true)
330
+ descendent
331
+ end
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
331
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
332
453
 
333
454
  # subscripts to return a child value identified by the given token.
334
455
  #
335
456
  # @param token [String, Integer, Object] an array index or hash key (JSON object property name)
336
457
  # of the instance identifying the child value
337
- # @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:
338
460
  #
339
- # - :auto (default): by default a JSI will be returned when either:
461
+ # - `:auto`: By default a JSI will be returned when either:
340
462
  #
341
- # - the result is a complex value (responds to #to_ary or #to_hash) and is described by some schemas
463
+ # - the result is a complex value (responds to #to_ary or #to_hash)
342
464
  # - the result is a schema (including true/false schemas)
343
465
  #
344
- # a plain value is returned when no schemas are known to describe the instance, or when the value is a
345
- # simple type (anything unresponsive to #to_ary / #to_hash).
466
+ # The plain content is returned when it is a simple type.
346
467
  #
347
- # - true: the result value will always be returned as a JSI. the #jsi_schemas of the result may be empty
348
- # if no schemas describe the instance.
468
+ # - true: the result value will always be returned as a JSI. the {#jsi_schemas} of the result may be
469
+ # empty if no schemas describe the instance.
349
470
  # - false: the result value will always be the plain instance.
350
471
  #
351
472
  # note that nil is returned (regardless of as_jsi) when there is no value to return because the token
352
473
  # is not a hash key or array index of the instance and no default value applies.
353
474
  # (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
354
475
  # unspecified behavior.)
355
- # @param use_default [true, false] whether to return a schema default value when the token is not in
356
- # 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
357
479
  # instance specifies a default value, that default is returned.
358
480
  #
359
481
  # if the result with the default value is a JSI (per the `as_jsi` param), that JSI is not a child of
@@ -364,48 +486,23 @@ module JSI
364
486
  # defaults are specified across those schemas), nil is returned.
365
487
  # (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
366
488
  # unspecified behavior.)
367
- # @return [JSI::Base, Object] the child value identified by the subscript token
368
- def [](token, as_jsi: :auto, use_default: true)
369
- if respond_to?(:to_hash)
370
- token_in_range = jsi_node_content_hash_pubsend(:key?, token)
371
- value = jsi_node_content_hash_pubsend(:[], token)
372
- elsif respond_to?(:to_ary)
373
- token_in_range = jsi_node_content_ary_pubsend(:each_index).include?(token)
374
- value = jsi_node_content_ary_pubsend(:[], token)
375
- else
376
- raise(CannotSubscriptError, "cannot subscript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
377
- end
378
-
379
- begin
380
- 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
381
494
 
382
- if token_in_range
383
- jsi_subinstance_as_jsi(value, subinstance_schemas, as_jsi) do
384
- jsi_subinstance_memos[token: token, subinstance_schemas: subinstance_schemas]
385
- end
386
- else
387
- if use_default
388
- defaults = Set.new
389
- subinstance_schemas.each do |subinstance_schema|
390
- if subinstance_schema.respond_to?(:to_hash) && subinstance_schema.key?('default')
391
- defaults << subinstance_schema['default']
392
- end
393
- end
394
- 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
395
500
 
396
- if use_default && defaults.size == 1
397
- # use the default value
398
- # we are using #dup so that we get a modified copy of self, in which we set dup[token]=default.
399
- dup.tap { |o| o[token] = defaults.first }[token, as_jsi: as_jsi]
400
- else
401
- # I kind of want to just return nil here. the preferred mechanism for
402
- # a JSI's default value should be its schema. but returning nil ignores
403
- # any value returned by Hash#default/#default_proc. there's no compelling
404
- # reason not to support both, so I'll return that.
405
- value
406
- end
407
- end
408
- 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
409
506
  end
410
507
 
411
508
  # assigns the subscript of the instance identified by the given token to the given value.
@@ -414,8 +511,8 @@ module JSI
414
511
  # @param token [String, Integer, Object] token identifying the subscript to assign
415
512
  # @param value [JSI::Base, Object] the value to be assigned
416
513
  def []=(token, value)
417
- unless respond_to?(:to_hash) || respond_to?(:to_ary)
418
- raise(NoMethodError, "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)
419
516
  end
420
517
  if value.is_a?(Base)
421
518
  self[token] = value.jsi_instance
@@ -427,12 +524,32 @@ module JSI
427
524
  # the set of JSI schema modules corresponding to the schemas that describe this JSI
428
525
  # @return [Set<Module>]
429
526
  def jsi_schema_modules
430
- jsi_schemas.map(&:jsi_schema_module).to_set.freeze
527
+ Util.ensure_module_set(jsi_schemas.map(&:jsi_schema_module))
528
+ end
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
431
548
  end
432
549
 
433
550
  # yields the content of this JSI's instance. the block must result in
434
- # a modified copy of the yielded instance (not destructively modifying it)
435
- # which will be used to instantiate a new JSI with the modified content.
551
+ # a modified copy of the yielded instance (not modified in place, which would alter this JSI
552
+ # as well) which will be used to instantiate and return a new JSI with the modified content.
436
553
  #
437
554
  # the result may have different schemas which describe it than this JSI's schemas,
438
555
  # if conditional applicator schemas apply differently to the modified instance.
@@ -441,48 +558,75 @@ module JSI
441
558
  # in a nondestructively modified copy of this.
442
559
  # @return [JSI::Base subclass] the modified copy of self
443
560
  def jsi_modified_copy(&block)
444
- if @jsi_ptr.root?
445
561
  modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
446
- self.class.new(Base::NOINSTANCE,
447
- jsi_document: modified_document,
448
- jsi_ptr: @jsi_ptr,
449
- jsi_schema_base_uri: @jsi_schema_base_uri,
450
- jsi_schema_resource_ancestors: @jsi_schema_resource_ancestors, # this can only be empty but included for consistency
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,
451
568
  )
452
- else
453
- modified_jsi_root_node = @jsi_root_node.jsi_modified_copy do |root|
454
- @jsi_ptr.modified_document_copy(root, &block)
455
- end
456
- @jsi_ptr.evaluate(modified_jsi_root_node, as_jsi: true)
457
- end
569
+ modified_jsi_root_node.jsi_descendent_node(@jsi_ptr)
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
458
600
  end
459
601
 
460
602
  # validates this JSI's instance against its schemas
461
603
  #
462
604
  # @return [JSI::Validation::FullResult]
463
605
  def jsi_validate
464
- jsi_schemas.instance_validate(self)
606
+ jsi_indicated_schemas.instance_validate(self)
465
607
  end
466
608
 
467
609
  # whether this JSI's instance is valid against all of its schemas
468
610
  # @return [Boolean]
469
611
  def jsi_valid?
470
- jsi_schemas.instance_valid?(self)
471
- end
472
-
473
- # @private
474
- def fully_validate(errors_as_objects: false)
475
- raise(NotImplementedError, "Base#fully_validate removed: see new validation interface Base#jsi_validate")
612
+ jsi_indicated_schemas.instance_valid?(self)
476
613
  end
477
614
 
478
- # @private
479
- def validate
480
- raise(NotImplementedError, "Base#validate renamed: see Base#jsi_valid?")
481
- end
482
-
483
- # @private
484
- def validate!
485
- raise(NotImplementedError, "Base#validate! removed")
615
+ # queries this JSI using the [JMESPath Ruby](https://rubygems.org/gems/jmespath) gem.
616
+ # see [https://jmespath.org/](https://jmespath.org/) to learn the JMESPath query language.
617
+ #
618
+ # the JMESPath gem is not a dependency of JSI, so must be installed / added to your Gemfile to use.
619
+ # e.g. `gem 'jmespath', '~> 1.5'`. note that versions below 1.5 are not compatible with JSI.
620
+ #
621
+ # @param expression [String] a [JMESPath](https://jmespath.org/) expression
622
+ # @param runtime_options passed to [JMESPath.search](https://rubydoc.info/gems/jmespath/JMESPath#search-class_method),
623
+ # though no runtime_options are publicly documented or normally used.
624
+ # @return [Array, Object, nil] query results.
625
+ # see [JMESPath.search](https://rubydoc.info/gems/jmespath/JMESPath#search-class_method)
626
+ def jmespath_search(expression, **runtime_options)
627
+ Util.require_jmespath
628
+
629
+ JMESPath.search(expression, self, **runtime_options)
486
630
  end
487
631
 
488
632
  def dup
@@ -492,7 +636,11 @@ module JSI
492
636
  # a string representing this JSI, indicating any named schemas and inspecting its instance
493
637
  # @return [String]
494
638
  def inspect
495
- "\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
639
+ -"\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
640
+ end
641
+
642
+ def to_s
643
+ inspect
496
644
  end
497
645
 
498
646
  # pretty-prints a representation of this JSI to the given printer
@@ -500,11 +648,9 @@ module JSI
500
648
  def pretty_print(q)
501
649
  q.text '#<'
502
650
  q.text jsi_object_group_text.join(' ')
503
- q.group_sub {
504
- q.nest(2) {
651
+ q.group(2) {
505
652
  q.breakable ' '
506
653
  q.pp jsi_instance
507
- }
508
654
  }
509
655
  q.breakable ''
510
656
  q.text '>'
@@ -513,107 +659,119 @@ module JSI
513
659
  # @private
514
660
  # @return [Array<String>]
515
661
  def jsi_object_group_text
516
- class_name = self.class.name unless self.class.in_schema_classes
517
- class_txt = begin
518
- if class_name
519
- # ignore ID
520
- schema_module_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name }.compact
521
- if schema_module_names.empty?
522
- class_name
523
- else
524
- "#{class_name} (#{schema_module_names.join(', ')})"
525
- end
526
- else
527
- schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name_from_ancestor || schema.schema_uri }.compact
528
- if schema_names.empty?
529
- "JSI"
530
- else
531
- "JSI (#{schema_names.join(', ')})"
532
- end
533
- end
662
+ schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name_from_ancestor || schema.schema_uri }.compact
663
+ if schema_names.empty?
664
+ class_txt = "JSI"
665
+ else
666
+ class_txt = -"JSI (#{schema_names.join(', ')})"
534
667
  end
535
668
 
536
- if (is_a?(PathedArrayNode) || is_a?(PathedHashNode)) && ![Array, Hash].include?(jsi_node_content.class)
669
+ if (is_a?(ArrayNode) || is_a?(HashNode)) && ![Array, Hash].include?(jsi_node_content.class)
537
670
  if jsi_node_content.respond_to?(:jsi_object_group_text)
538
671
  content_txt = jsi_node_content.jsi_object_group_text
539
672
  else
540
- content_txt = [jsi_node_content.class.to_s]
673
+ content_txt = jsi_node_content.class.to_s
541
674
  end
542
675
  else
543
- content_txt = []
676
+ content_txt = nil
544
677
  end
545
678
 
546
679
  [
547
680
  class_txt,
548
- is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
681
+ is_a?(Schema::MetaSchema) ? "Meta-Schema" : is_a?(Schema) ? "Schema" : nil,
549
682
  *content_txt,
550
- ].compact
683
+ ].compact.freeze
551
684
  end
552
685
 
553
- # a jsonifiable representation of the instance
554
- # @return [Object]
555
- def as_json(*opt)
556
- Typelike.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)
690
+ end
691
+
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)
557
697
  end
558
698
 
559
- # an opaque fingerprint of this JSI for {Util::FingerprintHash}.
699
+ # see {Util::Private::FingerprintHash}
700
+ # @api private
560
701
  def jsi_fingerprint
561
702
  {
562
- class: jsi_class,
703
+ class: JSI::Base,
704
+ jsi_schemas: jsi_schemas,
563
705
  jsi_document: jsi_document,
564
706
  jsi_ptr: jsi_ptr,
565
707
  # for instances in documents with schemas:
566
708
  jsi_resource_ancestor_uri: jsi_resource_ancestor_uri,
567
- # only defined for JSI::Schema instances:
568
- jsi_schema_instance_modules: is_a?(Schema) ? jsi_schema_instance_modules : nil,
569
- }
709
+ # different registries mean references may resolve to different resources so must not be equal
710
+ jsi_schema_registry: jsi_schema_registry,
711
+ }.freeze
570
712
  end
571
- include Util::FingerprintHash
572
713
 
573
714
  private
574
715
 
575
- def jsi_subinstance_schemas_memos
576
- jsi_memomap(:subinstance_schemas, key_by: -> (i) { i[:token] }) do |token: , instance: , subinstance: |
577
- SchemaSet.build do |schemas|
578
- jsi_schemas.each do |schema|
579
- schema.each_child_applicator_schema(token, instance) do |child_app_schema|
580
- child_app_schema.each_inplace_applicator_schema(subinstance) do |child_inpl_app_schema|
581
- schemas << child_inpl_app_schema
582
- end
583
- end
584
- end
585
- end
586
- 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))
587
720
  end
588
721
 
589
- def jsi_subinstance_memos
590
- jsi_memomap(:subinstance, key_by: -> (i) { i[:token] }) do |token: , subinstance_schemas: |
591
- JSI::SchemaClasses.class_for_schemas(subinstance_schemas).new(Base::NOINSTANCE,
592
- jsi_document: @jsi_document,
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
+ )
732
+ jsi_class.new(@jsi_document,
593
733
  jsi_ptr: @jsi_ptr[token],
594
- jsi_root_node: @jsi_root_node,
734
+ jsi_indicated_schemas: child_indicated_schemas,
595
735
  jsi_schema_base_uri: jsi_resource_ancestor_uri,
596
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,
597
740
  )
598
- end
599
741
  end
600
742
 
601
- def jsi_subinstance_as_jsi(value, subinstance_schemas, as_jsi)
602
- value_as_jsi = if [true, false].include?(as_jsi)
603
- 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)
752
+ if [true, false].include?(as_jsi)
753
+ child_as_jsi = as_jsi
604
754
  elsif as_jsi == :auto
605
- complex_value = subinstance_schemas.any? && (value.respond_to?(:to_hash) || value.respond_to?(:to_ary))
606
- schema_value = subinstance_schemas.any? { |subinstance_schema| subinstance_schema.describes_schema? }
607
- 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
608
758
  else
609
759
  raise(ArgumentError, "as_jsi must be one of: :auto, true, false")
610
760
  end
611
761
 
612
- if value_as_jsi
762
+ if child_as_jsi
613
763
  yield
614
764
  else
615
- value
765
+ child_content
616
766
  end
617
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
618
776
  end
619
777
  end