jsi 0.2.0 → 0.6.0

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