jsi 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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