jsi 0.2.0 → 0.6.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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +36 -0
  4. data/LICENSE.md +613 -0
  5. data/README.md +153 -52
  6. data/lib/jsi/base.rb +485 -338
  7. data/lib/jsi/jsi_coder.rb +24 -18
  8. data/lib/jsi/metaschema.rb +7 -0
  9. data/lib/jsi/metaschema_node/bootstrap_schema.rb +100 -0
  10. data/lib/jsi/metaschema_node.rb +245 -0
  11. data/lib/jsi/pathed_node.rb +49 -46
  12. data/lib/jsi/ptr.rb +292 -0
  13. data/lib/jsi/schema/application/child_application/contains.rb +16 -0
  14. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  15. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  16. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  17. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  18. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  19. data/lib/jsi/schema/application/child_application.rb +40 -0
  20. data/lib/jsi/schema/application/draft04.rb +8 -0
  21. data/lib/jsi/schema/application/draft06.rb +8 -0
  22. data/lib/jsi/schema/application/draft07.rb +8 -0
  23. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  24. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  25. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  26. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  27. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  28. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  29. data/lib/jsi/schema/application/inplace_application/someof.rb +29 -0
  30. data/lib/jsi/schema/application/inplace_application.rb +46 -0
  31. data/lib/jsi/schema/application.rb +12 -0
  32. data/lib/jsi/schema/draft04.rb +14 -0
  33. data/lib/jsi/schema/draft06.rb +14 -0
  34. data/lib/jsi/schema/draft07.rb +14 -0
  35. data/lib/jsi/schema/issue.rb +36 -0
  36. data/lib/jsi/schema/ref.rb +159 -0
  37. data/lib/jsi/schema/schema_ancestor_node.rb +119 -0
  38. data/lib/jsi/schema/validation/array.rb +69 -0
  39. data/lib/jsi/schema/validation/const.rb +20 -0
  40. data/lib/jsi/schema/validation/contains.rb +25 -0
  41. data/lib/jsi/schema/validation/core.rb +39 -0
  42. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  43. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  44. data/lib/jsi/schema/validation/draft04.rb +112 -0
  45. data/lib/jsi/schema/validation/draft06.rb +122 -0
  46. data/lib/jsi/schema/validation/draft07.rb +159 -0
  47. data/lib/jsi/schema/validation/enum.rb +25 -0
  48. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  49. data/lib/jsi/schema/validation/items.rb +54 -0
  50. data/lib/jsi/schema/validation/not.rb +20 -0
  51. data/lib/jsi/schema/validation/numeric.rb +121 -0
  52. data/lib/jsi/schema/validation/object.rb +45 -0
  53. data/lib/jsi/schema/validation/pattern.rb +34 -0
  54. data/lib/jsi/schema/validation/properties.rb +101 -0
  55. data/lib/jsi/schema/validation/property_names.rb +32 -0
  56. data/lib/jsi/schema/validation/ref.rb +40 -0
  57. data/lib/jsi/schema/validation/required.rb +27 -0
  58. data/lib/jsi/schema/validation/someof.rb +90 -0
  59. data/lib/jsi/schema/validation/string.rb +47 -0
  60. data/lib/jsi/schema/validation/type.rb +49 -0
  61. data/lib/jsi/schema/validation.rb +51 -0
  62. data/lib/jsi/schema.rb +528 -233
  63. data/lib/jsi/schema_classes.rb +238 -51
  64. data/lib/jsi/schema_registry.rb +141 -0
  65. data/lib/jsi/schema_set.rb +141 -0
  66. data/lib/jsi/simple_wrap.rb +8 -3
  67. data/lib/jsi/typelike_modules.rb +75 -68
  68. data/lib/jsi/util/attr_struct.rb +106 -0
  69. data/lib/jsi/util.rb +167 -64
  70. data/lib/jsi/validation/error.rb +34 -0
  71. data/lib/jsi/validation/result.rb +210 -0
  72. data/lib/jsi/validation.rb +15 -0
  73. data/lib/jsi/version.rb +3 -1
  74. data/lib/jsi.rb +72 -9
  75. data/lib/schemas/json-schema.org/draft-04/schema.rb +12 -0
  76. data/lib/schemas/json-schema.org/draft-06/schema.rb +12 -0
  77. data/lib/schemas/json-schema.org/draft-07/schema.rb +12 -0
  78. data/readme.rb +138 -0
  79. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  80. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  81. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  82. metadata +80 -107
  83. data/.simplecov +0 -1
  84. data/LICENSE.txt +0 -21
  85. data/Rakefile.rb +0 -9
  86. data/jsi.gemspec +0 -31
  87. data/lib/jsi/base/to_rb.rb +0 -126
  88. data/lib/jsi/json/node.rb +0 -243
  89. data/lib/jsi/json/pointer.rb +0 -330
  90. data/lib/jsi/json-schema-fragments.rb +0 -59
  91. data/lib/jsi/json.rb +0 -8
  92. data/test/base_array_test.rb +0 -209
  93. data/test/base_hash_test.rb +0 -204
  94. data/test/base_test.rb +0 -422
  95. data/test/jsi_coder_test.rb +0 -85
  96. data/test/jsi_json_arraynode_test.rb +0 -150
  97. data/test/jsi_json_hashnode_test.rb +0 -132
  98. data/test/jsi_json_node_test.rb +0 -310
  99. data/test/jsi_json_pointer_test.rb +0 -106
  100. data/test/jsi_test.rb +0 -11
  101. data/test/jsi_typelike_as_json_test.rb +0 -53
  102. data/test/schema_test.rb +0 -196
  103. data/test/spreedly_openapi_test.rb +0 -8
  104. data/test/test_helper.rb +0 -63
  105. data/test/util_test.rb +0 -62
data/lib/jsi/base.rb CHANGED
@@ -1,180 +1,324 @@
1
- require 'json'
2
- require 'jsi/typelike_modules'
1
+ # frozen_string_literal: true
3
2
 
4
3
  module JSI
5
- # the base class for representing and instantiating a JSON Schema.
4
+ # JSI::Base is the base class of every JSI instance of a JSON schema.
6
5
  #
7
- # a class inheriting from JSI::Base represents a JSON Schema. an instance of
8
- # that class represents a JSON schema instance.
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.
9
8
  #
10
- # as such, JSI::Base itself is not intended to be instantiated - subclasses
11
- # are dynamically created for schemas using {JSI.class_for_schema}, and these
12
- # are what are used to instantiate and represent JSON schema instances.
9
+ # a JSI instance of such a subclass represents a JSON schema instance described by that set of schemas.
10
+ #
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
16
  class Base
14
- include Memoize
15
- include Enumerable
16
17
  include PathedNode
18
+ include Schema::SchemaAncestorNode
19
+ include Util::Memoize
20
+
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
+
25
+ # an exception raised when #[] is invoked on an instance which is not an array or hash
26
+ class CannotSubscriptError < StandardError
27
+ end
17
28
 
18
29
  class << self
19
- # is the constant JSI::SchemaClasses::{self.schema_classes_const_name} defined?
20
- # (if so, we will prefer to use something more human-readable than that ugly mess.)
21
- attr_accessor :in_schema_classes
30
+ # @private @deprecated
31
+ def new_jsi(instance, **kw, &b)
32
+ new(instance, **kw, &b)
33
+ end
22
34
 
23
- # @return [String] absolute schema_id of the schema this class represents.
24
- # see {Schema#schema_id}.
25
- def schema_id
26
- schema.schema_id
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
27
42
  end
28
43
 
29
- # @return [String] a string representing the class, with schema_id
44
+ # a string indicating a class name if one is defined, as well as the schema module name
45
+ # and/or schema URI of each schema the class represents.
46
+ # @return [String]
30
47
  def inspect
31
- name # see #name for side effects
32
- if !respond_to?(:schema)
48
+ if !respond_to?(:jsi_class_schemas)
33
49
  super
34
- elsif in_schema_classes
35
- %Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
36
- elsif !name
37
- %Q(#<Class for Schema: #{schema_id}>)
38
50
  else
39
- %Q(#{name} (#{schema_id}))
40
- end
41
- end
51
+ 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
57
+ elsif schema.schema_uri
58
+ schema.schema_uri.to_s
59
+ else
60
+ schema.jsi_ptr.uri.to_s
61
+ end
62
+ end
42
63
 
43
- # @return [String] a string representing the class - a class name if one
44
- # was explicitly defined, otherwise a reference to JSI::SchemaClasses
45
- def to_s
46
- if !respond_to?(:schema)
47
- super
48
- elsif !name || name =~ /\AJSI::SchemaClasses::/
49
- %Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
50
- else
51
- name
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
70
+ else
71
+ if schema_names.empty?
72
+ "(JSI Schema Class for 0 schemas)"
73
+ else
74
+ "(JSI Schema Class: #{schema_names.join(', ')})"
75
+ end
76
+ end
52
77
  end
53
78
  end
54
79
 
55
- # @return [String] a name for a constant for this class, generated from the
56
- # schema_id. only used if the class is not assigned to another constant.
80
+ alias_method :to_s, :inspect
81
+
82
+ # @private
83
+ # see {.name}
57
84
  def schema_classes_const_name
58
- name = schema.schema_id.gsub(/[^\w]/, '_')
59
- name = 'X' + name unless name[/\A[a-zA-Z]/]
60
- name = name[0].upcase + name[1..-1]
61
- 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
62
99
  end
63
100
 
64
- # @return [String] a constant name of this class
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.
103
+ #
104
+ # this generated name is not too pretty but can be more helpful than an anonymous class, especially
105
+ # in error messages.
106
+ #
107
+ # @return [String]
65
108
  def name
66
- unless super || SchemaClasses.const_defined?(schema_classes_const_name)
67
- SchemaClasses.const_set(schema_classes_const_name, self)
68
- self.in_schema_classes = true
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
113
+ else
114
+ SchemaClasses.const_set(const_name, self)
115
+ @in_schema_classes = true
116
+ end
69
117
  end
70
118
  super
71
119
  end
72
120
  end
73
121
 
74
122
  # NOINSTANCE is a magic value passed to #initialize when instantiating a JSI
75
- # from a document and JSON Pointer.
76
- NOINSTANCE = Object.new.tap { |o| [:inspect, :to_s].each(&(-> (s, m) { o.define_singleton_method(m) { s } }.curry.([JSI::Base.name, 'NOINSTANCE'].join('::')))) }
123
+ # from a document and pointer.
124
+ #
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
77
129
 
78
130
  # initializes this JSI from the given instance - instance is most commonly
79
131
  # a parsed JSON document consisting of Hash, Array, or sometimes a basic
80
132
  # type, but this is in no way enforced and a JSI may wrap any object.
81
133
  #
82
- # @param instance [Object] the JSON Schema instance being represented
134
+ # @param instance [Object] the JSON Schema instance to be represented as a JSI
83
135
  # @param jsi_document [Object] for internal use. the instance may be specified as a
84
136
  # node in the `jsi_document` param, pointed to by `jsi_ptr`. the param `instance`
85
137
  # MUST be `NOINSTANCE` to use the jsi_document + jsi_ptr form. `jsi_document` MUST
86
138
  # NOT be passed if `instance` is anything other than `NOINSTANCE`.
87
- # @param jsi_ptr [JSI::JSON::Pointer] for internal use. a JSON pointer specifying
139
+ # @param jsi_ptr [JSI::Ptr] for internal use. a pointer specifying
88
140
  # the path of this instance in the `jsi_document` param. `jsi_ptr` must be passed
89
141
  # iff `jsi_document` is passed, i.e. when `instance` is `NOINSTANCE`
90
- # @param ancestor_jsi [JSI::Base] for internal use, specifies an ancestor_jsi
91
- # from which this JSI originated to calculate #parents
92
- def initialize(instance, jsi_document: nil, jsi_ptr: nil, ancestor_jsi: nil)
93
- unless respond_to?(:schema)
94
- raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #schema. please use JSI.class_for_schema")
142
+ # @param jsi_root_node [JSI::Base] for internal use, specifies the JSI at the root of the document
143
+ # @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,
149
+ jsi_schema_base_uri: nil,
150
+ jsi_schema_resource_ancestors: []
151
+ )
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.")
95
154
  end
96
155
 
97
- if instance.is_a?(JSI::Base)
98
- raise(TypeError, "assigning another JSI::Base instance to #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
99
- elsif instance.is_a?(JSI::Schema)
100
- raise(TypeError, "assigning a schema to #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
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}")
101
160
  end
102
161
 
162
+ jsi_initialize_memos
163
+
103
164
  if instance == NOINSTANCE
104
- @jsi_document = jsi_document
105
- unless jsi_ptr.is_a?(JSI::JSON::Pointer)
106
- raise(TypeError, "jsi_ptr must be a JSI::JSON::Pointer; got: #{jsi_ptr.inspect}")
107
- end
108
- @jsi_ptr = jsi_ptr
109
- else
110
- raise(Bug, 'incorrect usage') if jsi_document || jsi_ptr || ancestor_jsi
111
- if instance.is_a?(PathedNode)
112
- @jsi_document = instance.document_root_node
113
- # this can result in the unusual situation where ancestor_jsi is nil, though jsi_ptr is not root.
114
- # #document_root_node will then return a JSI::JSON::Pointer instead of a root JSI.
115
- @jsi_ptr = instance.node_ptr
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
116
170
  else
117
- @jsi_document = instance
118
- @jsi_ptr = JSI::JSON::Pointer.new([])
119
- end
120
- end
121
- if ancestor_jsi
122
- if !ancestor_jsi.is_a?(JSI::Base)
123
- raise(TypeError, "ancestor_jsi must be a JSI::Base; got: #{ancestor_jsi.inspect}")
124
- end
125
- if !ancestor_jsi.jsi_ptr.contains?(@jsi_ptr)
126
- raise(Bug, "ancestor_jsi ptr #{ancestor_jsi.jsi_ptr.inspect} is not ancestor of #{@jsi_ptr.inspect}")
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
127
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[]
183
+ @jsi_root_node = self
128
184
  end
129
- @ancestor_jsi = ancestor_jsi
185
+
186
+ self.jsi_schema_base_uri = jsi_schema_base_uri
187
+ self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors
130
188
 
131
189
  if self.jsi_instance.respond_to?(:to_hash)
132
- extend BaseHash
133
- elsif self.jsi_instance.respond_to?(:to_ary)
134
- extend BaseArray
190
+ extend PathedHashNode
191
+ end
192
+ if self.jsi_instance.respond_to?(:to_ary)
193
+ extend PathedArrayNode
135
194
  end
136
195
  end
137
196
 
138
- # document containing the instance of this JSI
197
+ # @!method jsi_schemas
198
+ # the set of schemas which describe this instance
199
+ # @return [JSI::SchemaSet]
200
+ # note: defined on subclasses by JSI::SchemaClasses.class_for_schemas
201
+
202
+
203
+ # document containing the instance of this JSI at our {#jsi_ptr}
139
204
  attr_reader :jsi_document
140
205
 
141
- # JSI::JSON::Pointer pointing to this JSI's instance within the jsi_document
206
+ # {JSI::Ptr} pointing to this JSI's instance within our {#jsi_document}
207
+ # @return [JSI::Ptr]
142
208
  attr_reader :jsi_ptr
143
209
 
144
- # a JSI which is an ancestor_jsi of this
145
- attr_reader :ancestor_jsi
210
+ # the JSI at the root of this JSI's document
211
+ # @return [JSI::Base]
212
+ attr_reader :jsi_root_node
146
213
 
147
- alias_method :node_document, :jsi_document
148
- alias_method :node_ptr, :jsi_ptr
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
149
216
 
150
- # the instance of the json-schema
151
- alias_method :jsi_instance, :node_content
152
- alias_method :instance, :node_content
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}"
221
+ end
153
222
 
154
- # each is overridden by BaseHash or BaseArray when appropriate. the base
155
- # #each is not actually implemented, along with all the methods of Enumerable.
156
- def each
157
- raise NoMethodError, "Enumerable methods and #each not implemented for instance that is not like a hash or array: #{instance.pretty_inspect.chomp}"
223
+ # yields a JSI of each node at or below this one in this JSI's document.
224
+ #
225
+ # returns an Enumerator if no block is given.
226
+ #
227
+ # @yield [JSI::Base] each node in the document, starting with self
228
+ # @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
231
+
232
+ 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)
240
+ end
241
+ end
242
+ nil
158
243
  end
159
244
 
160
- # an array of JSI instances above this one in the document. empty if this
161
- # JSI does not have a known ancestor.
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.
162
247
  #
163
- # @return [Array<JSI::Base>]
164
- def parent_jsis
165
- ancestor_jsi = @ancestor_jsi || self
166
- parent = ancestor_jsi
167
-
168
- (ancestor_jsi.jsi_ptr.reference_tokens.size...self.jsi_ptr.reference_tokens.size).map do |i|
169
- current = parent
170
- parent = parent[self.jsi_ptr.reference_tokens[i]]
171
- if current.is_a?(JSI::Base)
172
- current
248
+ # 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.
250
+ #
251
+ # @yield [JSI::Base] each child node below self
252
+ # @return [JSI::Base] modified copy of self containing only the selected nodes
253
+ def jsi_select_children_node_first(&block)
254
+ jsi_modified_copy do |instance|
255
+ if respond_to?(:to_hash)
256
+ res = instance.class.new
257
+ each_key do |k|
258
+ v = self[k, as_jsi: true]
259
+ 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
270
+ end
271
+ end
272
+ res
273
+ else
274
+ instance
275
+ end
276
+ end
277
+ end
278
+
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.
281
+ #
282
+ # this method recursively descends child nodes before yielding each node, so leaf nodes are yielded
283
+ # before their parents.
284
+ #
285
+ # @yield [JSI::Base] each child node below self
286
+ # @return [JSI::Base] modified copy of self containing only the selected nodes
287
+ def jsi_select_children_leaf_first(&block)
288
+ jsi_modified_copy do |instance|
289
+ if respond_to?(:to_hash)
290
+ res = instance.class.new
291
+ each_key do |k|
292
+ v = self[k, as_jsi: true].jsi_select_children_leaf_first(&block)
293
+ 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
304
+ end
305
+ end
306
+ res
173
307
  else
174
- # sometimes after a deref, we may end up with parents whose schema we do not know.
175
- # TODO this is kinda crap; hopefully we can remove it along with deref instantiating
176
- # a deref ptr as the same JSI class it is
177
- SimpleWrap.new(NOINSTANCE, jsi_document: jsi_document, jsi_ptr: jsi_ptr.take(i), ancestor_jsi: @ancestor_jsi)
308
+ instance
309
+ end
310
+ end
311
+ end
312
+
313
+ # an array of JSI instances above this one in the document.
314
+ #
315
+ # @return [Array<JSI::Base>]
316
+ def jsi_parent_nodes
317
+ parent = jsi_root_node
318
+
319
+ jsi_ptr.tokens.map do |token|
320
+ parent.tap do
321
+ parent = parent[token, as_jsi: true]
178
322
  end
179
323
  end.reverse
180
324
  end
@@ -182,291 +326,294 @@ module JSI
182
326
  # the immediate parent of this JSI. nil if there is no parent.
183
327
  #
184
328
  # @return [JSI::Base, nil]
185
- def parent_jsi
186
- parent_jsis.first
187
- end
188
-
189
- # @return [JSI::PathedNode] a pathed node at the root of the document. this is generally a JSI::Base
190
- # but may be a JSI::JSON::Node in unusual circumstances.
191
- def document_root_node
192
- if @jsi_ptr.root?
193
- self
194
- elsif @ancestor_jsi
195
- @ancestor_jsi.document_root_node
196
- elsif instance.is_a?(PathedNode)
197
- instance.document_root_node
198
- else
199
- JSI::JSON::Node.new_doc(@jsi_document)
200
- end
329
+ def jsi_parent_node
330
+ jsi_parent_nodes.first
201
331
  end
202
332
 
203
- # @return [JSI::PathedNode]
204
- def parent_node
205
- if @jsi_ptr.root?
206
- nil
207
- elsif @ancestor_jsi
208
- parent_jsis.first.tap do |parent_node|
209
- raise(Bug, 'is @ancestor_jsi == self? it should not be') if parent_node.nil?
210
- raise(Bug, "parent_node not PathedNode: #{parent_node.pretty_inspect.chomp}") unless parent_node.is_a?(JSI::PathedNode)
211
- end
212
- elsif instance.is_a?(PathedNode)
213
- instance.parent_node
333
+ # subscripts to return a child value identified by the given token.
334
+ #
335
+ # @param token [String, Integer, Object] an array index or hash key (JSON object property name)
336
+ # 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:
338
+ #
339
+ # - :auto (default): by default a JSI will be returned when either:
340
+ #
341
+ # - the result is a complex value (responds to #to_ary or #to_hash) and is described by some schemas
342
+ # - the result is a schema (including true/false schemas)
343
+ #
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).
346
+ #
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.
349
+ # - false: the result value will always be the plain instance.
350
+ #
351
+ # note that nil is returned (regardless of as_jsi) when there is no value to return because the token
352
+ # is not a hash key or array index of the instance and no default value applies.
353
+ # (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
354
+ # 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
357
+ # instance specifies a default value, that default is returned.
358
+ #
359
+ # if the result with the default value is a JSI (per the `as_jsi` param), that JSI is not a child of
360
+ # this JSI - this JSI is not modified to fill in the default value. the result is a JSI within a new
361
+ # document containing the filled-in default.
362
+ #
363
+ # if the child instance's schemas do not indicate a single default value (that is, if zero or multiple
364
+ # defaults are specified across those schemas), nil is returned.
365
+ # (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
366
+ # 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)
214
375
  else
215
- JSI::JSON::Node.new_by_type(@jsi_document, @jsi_ptr.parent)
376
+ raise(CannotSubscriptError, "cannot subscript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
216
377
  end
217
- end
218
378
 
219
- # @deprecated
220
- alias_method :parents, :parent_jsis
221
- # @deprecated
222
- alias_method :parent, :parent_jsi
379
+ begin
380
+ subinstance_schemas = jsi_subinstance_schemas_memos[token: token, instance: jsi_node_content, subinstance: value]
223
381
 
224
- # if this JSI is a $ref then the $ref is followed. otherwise this JSI
225
- # is returned.
226
- #
227
- # @yield [JSI::Base] if a block is given (optional), this will yield a deref'd JSI. if this
228
- # JSI is not a $ref object, the block is not called. if we are a $ref which cannot be followed
229
- # (e.g. a $ref to an external document, which is not yet supported), the block is not called.
230
- # @return [JSI::Base, self]
231
- def deref(&block)
232
- node_ptr_deref do |deref_ptr|
233
- jsi_from_root = deref_ptr.evaluate(document_root_node)
234
- if jsi_from_root.is_a?(JSI::Base)
235
- return jsi_from_root.tap(&(block || Util::NOOP))
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
236
386
  else
237
- # TODO I want to get rid of this ... just return jsi_from_root whatever it is
238
- # NOTE when I get rid of this, simplify #parent_jsis too
239
- if @ancestor_jsi && @ancestor_jsi.jsi_ptr.contains?(deref_ptr)
240
- derefed = self.class.new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: deref_ptr, ancestor_jsi: @ancestor_jsi)
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
395
+
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]
241
400
  else
242
- derefed = self.class.new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: deref_ptr)
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
243
406
  end
244
- return derefed.tap(&(block || Util::NOOP))
245
407
  end
246
408
  end
247
- return self
248
409
  end
249
410
 
250
- # yields the content of the underlying instance. the block must result in
251
- # a modified copy of that (not destructively modifying the yielded content)
252
- # which will be used to instantiate a new instance of this JSI class with
253
- # the modified content.
254
- # @yield [Object] the content of the instance. the block should result
255
- # in a (nondestructively) modified copy of this.
256
- # @return [JSI::Base subclass the same as self] the modified copy of self
257
- def modified_copy(&block)
258
- if @ancestor_jsi
259
- raise(Bug, 'bad @ancestor_jsi') if @ancestor_jsi.object_id == self.object_id
260
-
261
- modified_ancestor = @ancestor_jsi.modified_copy do |anc|
262
- mod_anc = @jsi_ptr.ptr_relative_to(@ancestor_jsi.jsi_ptr).modified_document_copy(anc, &block)
263
- mod_anc
264
- end
265
- self.class.new(Base::NOINSTANCE, jsi_document: modified_ancestor.jsi_document, jsi_ptr: @jsi_ptr, ancestor_jsi: modified_ancestor)
411
+ # assigns the subscript of the instance identified by the given token to the given value.
412
+ # if the value is a JSI, its instance is assigned instead of the JSI value itself.
413
+ #
414
+ # @param token [String, Integer, Object] token identifying the subscript to assign
415
+ # @param value [JSI::Base, Object] the value to be assigned
416
+ 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}")
419
+ end
420
+ if value.is_a?(Base)
421
+ self[token] = value.jsi_instance
266
422
  else
423
+ jsi_instance[token] = value
424
+ end
425
+ end
426
+
427
+ # the set of JSI schema modules corresponding to the schemas that describe this JSI
428
+ # @return [Set<Module>]
429
+ def jsi_schema_modules
430
+ jsi_schemas.map(&:jsi_schema_module).to_set.freeze
431
+ end
432
+
433
+ # 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.
436
+ #
437
+ # the result may have different schemas which describe it than this JSI's schemas,
438
+ # if conditional applicator schemas apply differently to the modified instance.
439
+ #
440
+ # @yield [Object] this JSI's instance. the block should result
441
+ # in a nondestructively modified copy of this.
442
+ # @return [JSI::Base subclass] the modified copy of self
443
+ def jsi_modified_copy(&block)
444
+ if @jsi_ptr.root?
267
445
  modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
268
- self.class.new(Base::NOINSTANCE, jsi_document: modified_document, jsi_ptr: @jsi_ptr)
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
451
+ )
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)
269
457
  end
270
458
  end
271
459
 
272
- # @return [String] the fragment representation of a pointer to this JSI's instance within its document
273
- def fragment
274
- @jsi_ptr.fragment
460
+ # validates this JSI's instance against its schemas
461
+ #
462
+ # @return [JSI::Validation::FullResult]
463
+ def jsi_validate
464
+ jsi_schemas.instance_validate(self)
275
465
  end
276
466
 
277
- # @return [Array<String>] array of schema validation error messages for this instance
278
- def fully_validate
279
- schema.fully_validate_instance(instance)
467
+ # whether this JSI's instance is valid against all of its schemas
468
+ # @return [Boolean]
469
+ def jsi_valid?
470
+ jsi_schemas.instance_valid?(self)
280
471
  end
281
472
 
282
- # @return [true, false] whether the instance validates against its schema
473
+ # @private
474
+ def fully_validate(errors_as_objects: false)
475
+ raise(NotImplementedError, "Base#fully_validate removed: see new validation interface Base#jsi_validate")
476
+ end
477
+
478
+ # @private
283
479
  def validate
284
- schema.validate_instance(instance)
480
+ raise(NotImplementedError, "Base#validate renamed: see Base#jsi_valid?")
285
481
  end
286
482
 
287
- # @return [true] if this method does not raise, it returns true to
288
- # indicate a valid instance.
289
- # @raise [::JSON::Schema::ValidationError] raises if the instance has
290
- # validation errors
483
+ # @private
291
484
  def validate!
292
- schema.validate_instance!(instance)
485
+ raise(NotImplementedError, "Base#validate! removed")
293
486
  end
294
487
 
295
488
  def dup
296
- modified_copy(&:dup)
489
+ jsi_modified_copy(&:dup)
297
490
  end
298
491
 
299
- # @return [String] a string representing this JSI, indicating its class
300
- # and inspecting its instance
492
+ # a string representing this JSI, indicating any named schemas and inspecting its instance
493
+ # @return [String]
301
494
  def inspect
302
- "\#<#{self.class.to_s} #{instance.inspect}>"
495
+ "\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
303
496
  end
304
497
 
305
- # pretty-prints a representation this JSI to the given printer
498
+ # pretty-prints a representation of this JSI to the given printer
306
499
  # @return [void]
307
500
  def pretty_print(q)
308
- q.instance_exec(self) do |obj|
309
- text "\#<#{obj.class.to_s}"
310
- group_sub {
311
- nest(2) {
312
- breakable ' '
313
- pp obj.instance
314
- }
501
+ q.text '#<'
502
+ q.text jsi_object_group_text.join(' ')
503
+ q.group_sub {
504
+ q.nest(2) {
505
+ q.breakable ' '
506
+ q.pp jsi_instance
315
507
  }
316
- breakable ''
317
- text '>'
318
- end
319
- end
320
-
321
- # @return [String] the instance's object_group_text
322
- def object_group_text
323
- instance.respond_to?(:object_group_text) ? instance.object_group_text : instance.class.inspect
324
- end
325
-
326
- # @return [Object] a jsonifiable representation of the instance
327
- def as_json(*opt)
328
- Typelike.as_json(instance, *opt)
329
- end
330
-
331
- # @return [Object] an opaque fingerprint of this JSI for FingerprintHash. JSIs are equal
332
- # if their instances are equal, and if the JSIs are of the same JSI class or subclass.
333
- def fingerprint
334
- {class: jsi_class, jsi_document: jsi_document, jsi_ptr: jsi_ptr}
508
+ }
509
+ q.breakable ''
510
+ q.text '>'
335
511
  end
336
- include FingerprintHash
337
512
 
338
- private
339
-
340
- # assigns a subscript, unwrapping a JSI if given.
341
- # @param subscript [Object] the bit between the [ and ]
342
- # @param value [JSI::Base, Object] the value to be assigned
343
- def subscript_assign(subscript, value)
344
- clear_memo(:[])
345
- if value.is_a?(Base)
346
- instance[subscript] = value.instance
347
- else
348
- instance[subscript] = value
349
- end
350
- end
351
-
352
- # this is an instance method in order to allow subclasses of JSI classes to
353
- # override it to point to other subclasses corresponding to other schemas.
354
- def class_for_schema(schema)
355
- JSI.class_for_schema(schema)
356
- end
357
- end
358
-
359
- # module extending a {JSI::Base} object when its instance is Hash-like (responds to #to_hash)
360
- module BaseHash
361
- include PathedHashNode
362
-
363
- alias_method :jsi_instance_hash_pubsend, :node_content_hash_pubsend
364
-
365
- # @param property_name [String, Object] the property name to subscript
366
- # @return [JSI::Base, Object] the instance's subscript value at the given
367
- # key property_name_. if there is a subschema defined for that property
368
- # on this JSI's schema, returns the instance's subscript as a JSI
369
- # instiation of that subschema.
370
- def [](property_name)
371
- instance_property_key_ = jsi_instance_hash_pubsend(:key?, property_name)
372
- if !instance_property_key_
373
- deref do |deref_jsi|
374
- return deref_jsi[property_name]
513
+ # @private
514
+ # @return [Array<String>]
515
+ 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
375
533
  end
376
534
  end
377
- instance_property_value_ = jsi_instance_sub(property_name)
378
- memoize(:[], property_name, instance_property_value_, instance_property_key_) do |property_name_, instance_property_value, instance_property_key|
379
- begin
380
- property_schema = schema.subschema_for_property(property_name_)
381
- property_schema = property_schema && property_schema.match_to_instance(instance_property_value)
382
535
 
383
- if !instance_property_key && property_schema && property_schema.schema_object.key?('default')
384
- # use the default value
385
- default = property_schema.schema_object['default']
386
- if default.respond_to?(:to_hash) || default.respond_to?(:to_ary)
387
- # we are using #dup so that we get a modified copy of self, in which we set dup[property_name_]=default.
388
- # this avoids duplication of code with #modified_copy and below in #[] to handle pathing and such.
389
- dup.tap { |o| o[property_name_] = default }[property_name_]
390
- else
391
- default
392
- end
393
- elsif property_schema && (instance_property_value.respond_to?(:to_hash) || instance_property_value.respond_to?(:to_ary))
394
- class_for_schema(property_schema).new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: @jsi_ptr[property_name_], ancestor_jsi: @ancestor_jsi || self)
395
- else
396
- instance_property_value
397
- end
536
+ if (is_a?(PathedArrayNode) || is_a?(PathedHashNode)) && ![Array, Hash].include?(jsi_node_content.class)
537
+ if jsi_node_content.respond_to?(:jsi_object_group_text)
538
+ content_txt = jsi_node_content.jsi_object_group_text
539
+ else
540
+ content_txt = [jsi_node_content.class.to_s]
398
541
  end
542
+ else
543
+ content_txt = []
399
544
  end
400
- end
401
545
 
402
- # assigns the given property name of the instance to the given value.
403
- # if the value is a JSI, its instance is assigned.
404
- # @param property_name [Object] this should generally be a String, but JSI
405
- # does not enforce any constraint on it.
406
- # @param value [Object] the value to be assigned to the given subscript
407
- # property_name
408
- def []=(property_name, value)
409
- subscript_assign(property_name, value)
546
+ [
547
+ class_txt,
548
+ is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
549
+ *content_txt,
550
+ ].compact
410
551
  end
411
552
 
412
- private
413
-
414
- # @param token [String, Object]
553
+ # a jsonifiable representation of the instance
415
554
  # @return [Object]
416
- def jsi_instance_sub(token)
417
- jsi_instance_hash_pubsend(:[], token)
555
+ def as_json(*opt)
556
+ Typelike.as_json(jsi_instance, *opt)
418
557
  end
419
- end
420
-
421
- # module extending a {JSI::Base} object when its instance is Array-like (responds to #to_ary)
422
- module BaseArray
423
- include PathedArrayNode
424
558
 
425
- alias_method :jsi_instance_ary_pubsend, :node_content_ary_pubsend
559
+ # an opaque fingerprint of this JSI for {Util::FingerprintHash}.
560
+ def jsi_fingerprint
561
+ {
562
+ class: jsi_class,
563
+ jsi_document: jsi_document,
564
+ jsi_ptr: jsi_ptr,
565
+ # for instances in documents with schemas:
566
+ 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
+ }
570
+ end
571
+ include Util::FingerprintHash
426
572
 
427
- # @param i [Integer] the array index to subscript
428
- # @return [JSI::Base, Object] the instance's subscript value at the given index
429
- # i. if there is a subschema defined for that index on this JSI's schema,
430
- # returns the instance's subscript as a JSI instiation of that subschema.
431
- def [](i)
432
- memoize(:[], i, jsi_instance_sub(i), jsi_instance_ary_pubsend(:each_index).to_a.include?(i)) do |i_, instance_idx_value, i_in_range|
433
- begin
434
- index_schema = schema.subschema_for_index(i_)
435
- index_schema = index_schema && index_schema.match_to_instance(instance_idx_value)
573
+ private
436
574
 
437
- if !i_in_range && index_schema && index_schema.schema_object.key?('default')
438
- # use the default value
439
- default = index_schema.schema_object['default']
440
- if default.respond_to?(:to_hash) || default.respond_to?(:to_ary)
441
- # we are using #dup so that we get a modified copy of self, in which we set dup[i]=default.
442
- # this avoids duplication of code with #modified_copy and below in #[] to handle pathing and such.
443
- dup.tap { |o| o[i_] = default }[i_]
444
- else
445
- default
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
446
583
  end
447
- elsif index_schema && (instance_idx_value.respond_to?(:to_hash) || instance_idx_value.respond_to?(:to_ary))
448
- class_for_schema(index_schema).new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: @jsi_ptr[i_], ancestor_jsi: @ancestor_jsi || self)
449
- else
450
- instance_idx_value
451
584
  end
452
585
  end
453
586
  end
454
587
  end
455
588
 
456
- # assigns the given index of the instance to the given value.
457
- # if the value is a JSI, its instance is assigned.
458
- # @param i [Object] the array index to assign
459
- # @param value [Object] the value to be assigned to the given subscript i
460
- def []=(i, value)
461
- subscript_assign(i, value)
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,
593
+ jsi_ptr: @jsi_ptr[token],
594
+ jsi_root_node: @jsi_root_node,
595
+ jsi_schema_base_uri: jsi_resource_ancestor_uri,
596
+ jsi_schema_resource_ancestors: is_a?(Schema) ? jsi_subschema_resource_ancestors : jsi_schema_resource_ancestors,
597
+ )
598
+ end
462
599
  end
463
600
 
464
- private
601
+ def jsi_subinstance_as_jsi(value, subinstance_schemas, as_jsi)
602
+ value_as_jsi = if [true, false].include?(as_jsi)
603
+ as_jsi
604
+ 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
608
+ else
609
+ raise(ArgumentError, "as_jsi must be one of: :auto, true, false")
610
+ end
465
611
 
466
- # @param token [Integer]
467
- # @return [Object]
468
- def jsi_instance_sub(token)
469
- jsi_instance_ary_pubsend(:[], token)
612
+ if value_as_jsi
613
+ yield
614
+ else
615
+ value
616
+ end
470
617
  end
471
618
  end
472
619
  end