jsi 0.4.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 +15 -0
  4. data/README.md +105 -38
  5. data/lib/jsi/base.rb +349 -155
  6. data/lib/jsi/jsi_coder.rb +5 -4
  7. data/lib/jsi/metaschema_node/bootstrap_schema.rb +100 -0
  8. data/lib/jsi/metaschema_node.rb +156 -129
  9. data/lib/jsi/pathed_node.rb +47 -49
  10. data/lib/jsi/ptr.rb +292 -0
  11. data/lib/jsi/schema/application/child_application/contains.rb +16 -0
  12. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  13. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  14. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  15. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  16. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  17. data/lib/jsi/schema/application/child_application.rb +40 -0
  18. data/lib/jsi/schema/application/draft04.rb +8 -0
  19. data/lib/jsi/schema/application/draft06.rb +8 -0
  20. data/lib/jsi/schema/application/draft07.rb +8 -0
  21. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  22. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  23. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  24. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  25. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  26. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  27. data/lib/jsi/schema/application/inplace_application/someof.rb +29 -0
  28. data/lib/jsi/schema/application/inplace_application.rb +46 -0
  29. data/lib/jsi/schema/application.rb +12 -0
  30. data/lib/jsi/schema/draft04.rb +14 -0
  31. data/lib/jsi/schema/draft06.rb +14 -0
  32. data/lib/jsi/schema/draft07.rb +14 -0
  33. data/lib/jsi/schema/issue.rb +36 -0
  34. data/lib/jsi/schema/ref.rb +159 -0
  35. data/lib/jsi/schema/schema_ancestor_node.rb +119 -0
  36. data/lib/jsi/schema/validation/array.rb +69 -0
  37. data/lib/jsi/schema/validation/const.rb +20 -0
  38. data/lib/jsi/schema/validation/contains.rb +25 -0
  39. data/lib/jsi/schema/validation/core.rb +39 -0
  40. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  41. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  42. data/lib/jsi/schema/validation/draft04.rb +112 -0
  43. data/lib/jsi/schema/validation/draft06.rb +122 -0
  44. data/lib/jsi/schema/validation/draft07.rb +159 -0
  45. data/lib/jsi/schema/validation/enum.rb +25 -0
  46. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  47. data/lib/jsi/schema/validation/items.rb +54 -0
  48. data/lib/jsi/schema/validation/not.rb +20 -0
  49. data/lib/jsi/schema/validation/numeric.rb +121 -0
  50. data/lib/jsi/schema/validation/object.rb +45 -0
  51. data/lib/jsi/schema/validation/pattern.rb +34 -0
  52. data/lib/jsi/schema/validation/properties.rb +101 -0
  53. data/lib/jsi/schema/validation/property_names.rb +32 -0
  54. data/lib/jsi/schema/validation/ref.rb +40 -0
  55. data/lib/jsi/schema/validation/required.rb +27 -0
  56. data/lib/jsi/schema/validation/someof.rb +90 -0
  57. data/lib/jsi/schema/validation/string.rb +47 -0
  58. data/lib/jsi/schema/validation/type.rb +49 -0
  59. data/lib/jsi/schema/validation.rb +51 -0
  60. data/lib/jsi/schema.rb +486 -133
  61. data/lib/jsi/schema_classes.rb +157 -42
  62. data/lib/jsi/schema_registry.rb +141 -0
  63. data/lib/jsi/schema_set.rb +141 -0
  64. data/lib/jsi/simple_wrap.rb +2 -2
  65. data/lib/jsi/typelike_modules.rb +52 -37
  66. data/lib/jsi/util/attr_struct.rb +106 -0
  67. data/lib/jsi/util.rb +141 -25
  68. data/lib/jsi/validation/error.rb +34 -0
  69. data/lib/jsi/validation/result.rb +210 -0
  70. data/lib/jsi/validation.rb +15 -0
  71. data/lib/jsi/version.rb +3 -1
  72. data/lib/jsi.rb +55 -9
  73. data/lib/schemas/json-schema.org/draft-04/schema.rb +8 -3
  74. data/lib/schemas/json-schema.org/draft-06/schema.rb +8 -3
  75. data/lib/schemas/json-schema.org/draft-07/schema.rb +12 -0
  76. data/readme.rb +138 -0
  77. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  78. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  79. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  80. metadata +69 -118
  81. data/.simplecov +0 -3
  82. data/Rakefile.rb +0 -9
  83. data/jsi.gemspec +0 -28
  84. data/lib/jsi/base/to_rb.rb +0 -128
  85. data/lib/jsi/json/node.rb +0 -203
  86. data/lib/jsi/json/pointer.rb +0 -419
  87. data/lib/jsi/json-schema-fragments.rb +0 -61
  88. data/lib/jsi/json.rb +0 -10
  89. data/resources/icons/AGPL-3.0.png +0 -0
  90. data/test/base_array_test.rb +0 -323
  91. data/test/base_hash_test.rb +0 -337
  92. data/test/base_test.rb +0 -486
  93. data/test/jsi_coder_test.rb +0 -85
  94. data/test/jsi_json_arraynode_test.rb +0 -150
  95. data/test/jsi_json_hashnode_test.rb +0 -132
  96. data/test/jsi_json_node_test.rb +0 -257
  97. data/test/jsi_json_pointer_test.rb +0 -102
  98. data/test/jsi_test.rb +0 -11
  99. data/test/jsi_typelike_as_json_test.rb +0 -53
  100. data/test/metaschema_node_test.rb +0 -19
  101. data/test/schema_module_test.rb +0 -21
  102. data/test/schema_test.rb +0 -208
  103. data/test/spreedly_openapi_test.rb +0 -8
  104. data/test/test_helper.rb +0 -97
  105. data/test/util_test.rb +0 -62
data/lib/jsi/base.rb CHANGED
@@ -1,30 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSI
4
- # 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.
5
5
  #
6
- # a class inheriting from JSI::Base represents a JSON Schema. an instance of
7
- # 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.
8
8
  #
9
- # as such, JSI::Base itself is not intended to be instantiated - subclasses
10
- # are dynamically created for schemas using {JSI.class_for_schema}, and these
11
- # 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.
12
16
  class Base
17
+ include PathedNode
18
+ include Schema::SchemaAncestorNode
13
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.
14
23
  include Enumerable
15
- include PathedNode
24
+
25
+ # an exception raised when #[] is invoked on an instance which is not an array or hash
16
26
  class CannotSubscriptError < StandardError
17
27
  end
18
28
 
19
29
  class << self
20
- # JSI::Base.new_jsi behaves the same as .new, and is defined for compatibility so you may call #new_jsi
21
- # on any of a JSI::Schema, a JSI::SchemaModule, or a JSI schema class.
22
- # @return [JSI::Base] a JSI whose instance is the given instance
23
- def new_jsi(instance, *a, &b)
24
- new(instance, *a, &b)
30
+ # @private @deprecated
31
+ def new_jsi(instance, **kw, &b)
32
+ new(instance, **kw, &b)
25
33
  end
26
34
 
27
- # is the constant JSI::SchemaClasses::{self.schema_classes_const_name} defined?
35
+ # @private
36
+ # is the constant JSI::SchemaClasses::<self.schema_classes_const_name> defined?
28
37
  # (if so, we will prefer to use something more human-readable than that ugly mess.)
29
38
  def in_schema_classes
30
39
  # #name sets @in_schema_classes
@@ -32,22 +41,23 @@ module JSI
32
41
  @in_schema_classes
33
42
  end
34
43
 
35
- # @return [String] a string representing the class, indicating the schemas represented by their module
36
- # name or a URI
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]
37
47
  def inspect
38
48
  if !respond_to?(:jsi_class_schemas)
39
49
  super
40
50
  else
41
51
  schema_names = jsi_class_schemas.map do |schema|
42
52
  mod = schema.jsi_schema_module
43
- if mod.name && schema.schema_id
44
- "#{mod.name} (#{schema.schema_id})"
53
+ if mod.name && schema.schema_uri
54
+ "#{mod.name} (#{schema.schema_uri})"
45
55
  elsif mod.name
46
56
  mod.name
47
- elsif schema.schema_id
48
- schema.schema_id
57
+ elsif schema.schema_uri
58
+ schema.schema_uri.to_s
49
59
  else
50
- schema.node_ptr.uri
60
+ schema.jsi_ptr.uri.to_s
51
61
  end
52
62
  end
53
63
 
@@ -69,27 +79,32 @@ module JSI
69
79
 
70
80
  alias_method :to_s, :inspect
71
81
 
72
- # @return [String, nil] a name for a constant for this class, generated from the constant name
73
- # or schema id of each schema this class represents. nil if any represented schema has no constant
74
- # name or schema id.
82
+ # @private
83
+ # see {.name}
75
84
  def schema_classes_const_name
76
85
  if respond_to?(:jsi_class_schemas)
77
86
  schema_names = jsi_class_schemas.map do |schema|
78
87
  if schema.jsi_schema_module.name
79
88
  schema.jsi_schema_module.name
80
- elsif schema.schema_id
81
- schema.schema_id
89
+ elsif schema.schema_uri
90
+ schema.schema_uri.to_s
82
91
  else
83
92
  nil
84
93
  end
85
94
  end
86
95
  if !schema_names.any?(&:nil?) && !schema_names.empty?
87
- schema_names.sort.map { |n| 'X' + n.gsub(/[^\w]/, '_') }.join('')
96
+ schema_names.sort.map { |n| 'X' + n.to_s.gsub(/[^\w]/, '_') }.join('')
88
97
  end
89
98
  end
90
99
  end
91
100
 
92
- # @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]
93
108
  def name
94
109
  unless instance_variable_defined?(:@in_schema_classes)
95
110
  const_name = schema_classes_const_name
@@ -105,23 +120,35 @@ module JSI
105
120
  end
106
121
 
107
122
  # NOINSTANCE is a magic value passed to #initialize when instantiating a JSI
108
- # from a document and JSON Pointer.
109
- 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
110
129
 
111
130
  # initializes this JSI from the given instance - instance is most commonly
112
131
  # a parsed JSON document consisting of Hash, Array, or sometimes a basic
113
132
  # type, but this is in no way enforced and a JSI may wrap any object.
114
133
  #
115
- # @param instance [Object] the JSON Schema instance being represented
134
+ # @param instance [Object] the JSON Schema instance to be represented as a JSI
116
135
  # @param jsi_document [Object] for internal use. the instance may be specified as a
117
136
  # node in the `jsi_document` param, pointed to by `jsi_ptr`. the param `instance`
118
137
  # MUST be `NOINSTANCE` to use the jsi_document + jsi_ptr form. `jsi_document` MUST
119
138
  # NOT be passed if `instance` is anything other than `NOINSTANCE`.
120
- # @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
121
140
  # the path of this instance in the `jsi_document` param. `jsi_ptr` must be passed
122
141
  # iff `jsi_document` is passed, i.e. when `instance` is `NOINSTANCE`
123
142
  # @param jsi_root_node [JSI::Base] for internal use, specifies the JSI at the root of the document
124
- def initialize(instance, jsi_document: nil, jsi_ptr: nil, jsi_root_node: nil)
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
+ )
125
152
  unless respond_to?(:jsi_schemas)
126
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.")
127
154
  end
@@ -132,12 +159,11 @@ module JSI
132
159
  raise(TypeError, "assigning another JSI::Base instance to a #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
133
160
  end
134
161
 
162
+ jsi_initialize_memos
163
+
135
164
  if instance == NOINSTANCE
136
- @jsi_document = jsi_document
137
- unless jsi_ptr.is_a?(JSI::JSON::Pointer)
138
- raise(TypeError, "jsi_ptr must be a JSI::JSON::Pointer; got: #{jsi_ptr.inspect}")
139
- end
140
- @jsi_ptr = jsi_ptr
165
+ self.jsi_document = jsi_document
166
+ self.jsi_ptr = jsi_ptr
141
167
  if @jsi_ptr.root?
142
168
  raise(Bug, "jsi_root_node cannot be specified for root JSI") if jsi_root_node
143
169
  @jsi_root_node = self
@@ -153,55 +179,146 @@ module JSI
153
179
  else
154
180
  raise(Bug, 'incorrect usage') if jsi_document || jsi_ptr || jsi_root_node
155
181
  @jsi_document = instance
156
- @jsi_ptr = JSI::JSON::Pointer[]
182
+ @jsi_ptr = Ptr[]
157
183
  @jsi_root_node = self
158
184
  end
159
185
 
186
+ self.jsi_schema_base_uri = jsi_schema_base_uri
187
+ self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors
188
+
160
189
  if self.jsi_instance.respond_to?(:to_hash)
161
190
  extend PathedHashNode
162
- elsif self.jsi_instance.respond_to?(:to_ary)
163
- extend PathedArrayNode
164
191
  end
165
-
166
- jsi_schemas.each do |schema|
167
- if schema.describes_schema?
168
- extend JSI::Schema
169
- end
192
+ if self.jsi_instance.respond_to?(:to_ary)
193
+ extend PathedArrayNode
170
194
  end
171
195
  end
172
196
 
173
- # 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}
174
204
  attr_reader :jsi_document
175
205
 
176
- # 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]
177
208
  attr_reader :jsi_ptr
178
209
 
179
210
  # the JSI at the root of this JSI's document
211
+ # @return [JSI::Base]
180
212
  attr_reader :jsi_root_node
181
213
 
182
- alias_method :node_document, :jsi_document
183
- alias_method :node_ptr, :jsi_ptr
184
- alias_method :document_root_node, :jsi_root_node
185
-
186
- # the instance of the json-schema - the underlying JSON data used to instantiate this JSI
187
- alias_method :jsi_instance, :node_content
188
- alias_method :instance, :node_content
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
189
216
 
190
- # each is overridden by PathedHashNode or PathedArrayNode when appropriate. the base
191
- # #each is not actually implemented, along with all the methods of Enumerable.
192
- def each
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(*_)
193
220
  raise NoMethodError, "Enumerable methods and #each not implemented for instance that is not like a hash or array: #{jsi_instance.pretty_inspect.chomp}"
194
221
  end
195
222
 
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
243
+ end
244
+
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.
247
+ #
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
307
+ else
308
+ instance
309
+ end
310
+ end
311
+ end
312
+
196
313
  # an array of JSI instances above this one in the document.
197
314
  #
198
315
  # @return [Array<JSI::Base>]
199
- def parent_jsis
316
+ def jsi_parent_nodes
200
317
  parent = jsi_root_node
201
318
 
202
- jsi_ptr.reference_tokens.map do |token|
319
+ jsi_ptr.tokens.map do |token|
203
320
  parent.tap do
204
- parent = parent[token]
321
+ parent = parent[token, as_jsi: true]
205
322
  end
206
323
  end.reverse
207
324
  end
@@ -209,62 +326,77 @@ module JSI
209
326
  # the immediate parent of this JSI. nil if there is no parent.
210
327
  #
211
328
  # @return [JSI::Base, nil]
212
- def parent_jsi
213
- parent_jsis.first
329
+ def jsi_parent_node
330
+ jsi_parent_nodes.first
214
331
  end
215
332
 
216
- alias_method :parent_node, :parent_jsi
217
-
218
- # @deprecated
219
- alias_method :parents, :parent_jsis
220
- # @deprecated
221
- alias_method :parent, :parent_jsi
222
-
223
- # @param token [String, Integer, Object] the token to subscript
224
- # @return [JSI::Base, Object] the instance's subscript value at the given token.
225
- # if this JSI's schemas define subschemas which apply for the given token, and the value is complex,
226
- # returns the subscript value as a JSI instantiation of those subschemas. otherwise, the plain instance
227
- # value is returned.
228
- def [](token)
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)
229
369
  if respond_to?(:to_hash)
230
- token_in_range = node_content_hash_pubsend(:key?, token)
231
- value = node_content_hash_pubsend(:[], token)
370
+ token_in_range = jsi_node_content_hash_pubsend(:key?, token)
371
+ value = jsi_node_content_hash_pubsend(:[], token)
232
372
  elsif respond_to?(:to_ary)
233
- token_in_range = node_content_ary_pubsend(:each_index).include?(token)
234
- value = node_content_ary_pubsend(:[], token)
373
+ token_in_range = jsi_node_content_ary_pubsend(:each_index).include?(token)
374
+ value = jsi_node_content_ary_pubsend(:[], token)
235
375
  else
236
- raise(CannotSubscriptError, "cannot subcript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
376
+ raise(CannotSubscriptError, "cannot subscript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
237
377
  end
238
378
 
239
- result = jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range|
240
- if respond_to?(:to_ary)
241
- token_schemas = jsi_schemas.map { |schema| schema.subschemas_for_index(token) }.inject(Set.new, &:|)
242
- else
243
- token_schemas = jsi_schemas.map { |schema| schema.subschemas_for_property_name(token) }.inject(Set.new, &:|)
244
- end
245
- token_schemas = token_schemas.map { |schema| schema.match_to_instance(value) }.inject(Set.new, &:|)
379
+ begin
380
+ subinstance_schemas = jsi_subinstance_schemas_memos[token: token, instance: jsi_node_content, subinstance: value]
246
381
 
247
382
  if token_in_range
248
- complex_value = token_schemas.any? && (value.respond_to?(:to_hash) || value.respond_to?(:to_ary))
249
- schema_value = token_schemas.any? { |token_schema| token_schema.describes_schema? }
250
-
251
- if complex_value || schema_value
252
- JSI::SchemaClasses.class_for_schemas(token_schemas).new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: @jsi_ptr[token], jsi_root_node: @jsi_root_node)
253
- else
254
- value
383
+ jsi_subinstance_as_jsi(value, subinstance_schemas, as_jsi) do
384
+ jsi_subinstance_memos[token: token, subinstance_schemas: subinstance_schemas]
255
385
  end
256
386
  else
257
- defaults = Set.new
258
- token_schemas.each do |token_schema|
259
- if token_schema.respond_to?(:to_hash) && token_schema.key?('default')
260
- defaults << token_schema['default']
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
261
393
  end
262
394
  end
263
395
 
264
- if defaults.size == 1
396
+ if use_default && defaults.size == 1
265
397
  # use the default value
266
398
  # we are using #dup so that we get a modified copy of self, in which we set dup[token]=default.
267
- dup.tap { |o| o[token] = defaults.first }[token]
399
+ dup.tap { |o| o[token] = defaults.first }[token, as_jsi: as_jsi]
268
400
  else
269
401
  # I kind of want to just return nil here. the preferred mechanism for
270
402
  # a JSI's default value should be its schema. but returning nil ignores
@@ -274,7 +406,6 @@ module JSI
274
406
  end
275
407
  end
276
408
  end
277
- result
278
409
  end
279
410
 
280
411
  # assigns the subscript of the instance identified by the given token to the given value.
@@ -284,9 +415,8 @@ module JSI
284
415
  # @param value [JSI::Base, Object] the value to be assigned
285
416
  def []=(token, value)
286
417
  unless respond_to?(:to_hash) || respond_to?(:to_ary)
287
- raise(NoMethodError, "cannot assign subcript (using token: #{token.inspect}) to instance: #{jsi_instance.pretty_inspect.chomp}")
418
+ raise(NoMethodError, "cannot assign subscript (using token: #{token.inspect}) to instance: #{jsi_instance.pretty_inspect.chomp}")
288
419
  end
289
- jsi_clear_memo(:[])
290
420
  if value.is_a?(Base)
291
421
  self[token] = value.jsi_instance
292
422
  else
@@ -294,73 +424,82 @@ module JSI
294
424
  end
295
425
  end
296
426
 
297
- # if this JSI is a $ref then the $ref is followed. otherwise this JSI
298
- # is returned.
299
- #
300
- # @yield [JSI::Base] if a block is given (optional), this will yield a deref'd JSI. if this
301
- # JSI is not a $ref object, the block is not called. if we are a $ref which cannot be followed
302
- # (e.g. a $ref to an external document, which is not yet supported), the block is not called.
303
- # @return [JSI::Base, self]
304
- def deref(&block)
305
- node_ptr_deref do |deref_ptr|
306
- deref_ptr.evaluate(jsi_root_node).tap(&(block || Util::NOOP))
307
- end
308
- return self
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
309
431
  end
310
432
 
311
- # yields the content of the underlying instance. the block must result in
312
- # a modified copy of that (not destructively modifying the yielded content)
313
- # which will be used to instantiate a new instance of this JSI class with
314
- # the modified content.
315
- # @yield [Object] the content of the instance. the block should result
316
- # in a (nondestructively) modified copy of this.
317
- # @return [JSI::Base subclass the same as self] the modified copy of self
318
- def modified_copy(&block)
319
- if node_ptr.root?
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?
320
445
  modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
321
- 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
+ )
322
452
  else
323
- modified_jsi_root_node = @jsi_root_node.modified_copy do |root|
453
+ modified_jsi_root_node = @jsi_root_node.jsi_modified_copy do |root|
324
454
  @jsi_ptr.modified_document_copy(root, &block)
325
455
  end
326
- self.class.new(Base::NOINSTANCE, jsi_document: modified_jsi_root_node.node_document, jsi_ptr: @jsi_ptr, jsi_root_node: modified_jsi_root_node)
456
+ @jsi_ptr.evaluate(modified_jsi_root_node, as_jsi: true)
327
457
  end
328
458
  end
329
459
 
330
- # @return [Array] array of schema validation errors for this instance
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)
465
+ end
466
+
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)
471
+ end
472
+
473
+ # @private
331
474
  def fully_validate(errors_as_objects: false)
332
- jsi_schemas.map { |schema| schema.fully_validate_instance(jsi_instance, errors_as_objects: errors_as_objects) }.inject([], &:+)
475
+ raise(NotImplementedError, "Base#fully_validate removed: see new validation interface Base#jsi_validate")
333
476
  end
334
477
 
335
- # @return [true, false] whether the instance validates against its schema
478
+ # @private
336
479
  def validate
337
- jsi_schemas.all? { |schema| schema.validate_instance(jsi_instance) }
480
+ raise(NotImplementedError, "Base#validate renamed: see Base#jsi_valid?")
338
481
  end
339
482
 
340
- # @return [true] if this method does not raise, it returns true to
341
- # indicate a valid instance.
342
- # @raise [::JSON::Schema::ValidationError] raises if the instance has
343
- # validation errors
483
+ # @private
344
484
  def validate!
345
- jsi_schemas.each { |schema| schema.validate_instance!(jsi_instance) }
346
- true
485
+ raise(NotImplementedError, "Base#validate! removed")
347
486
  end
348
487
 
349
488
  def dup
350
- modified_copy(&:dup)
489
+ jsi_modified_copy(&:dup)
351
490
  end
352
491
 
353
- # @return [String] a string representing this JSI, indicating its class
354
- # and inspecting its instance
492
+ # a string representing this JSI, indicating any named schemas and inspecting its instance
493
+ # @return [String]
355
494
  def inspect
356
- "\#<#{object_group_text.join(' ')} #{jsi_instance.inspect}>"
495
+ "\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
357
496
  end
358
497
 
359
- # pretty-prints a representation this JSI to the given printer
498
+ # pretty-prints a representation of this JSI to the given printer
360
499
  # @return [void]
361
500
  def pretty_print(q)
362
501
  q.text '#<'
363
- q.text object_group_text.join(' ')
502
+ q.text jsi_object_group_text.join(' ')
364
503
  q.group_sub {
365
504
  q.nest(2) {
366
505
  q.breakable ' '
@@ -371,8 +510,9 @@ module JSI
371
510
  q.text '>'
372
511
  end
373
512
 
513
+ # @private
374
514
  # @return [Array<String>]
375
- def object_group_text
515
+ def jsi_object_group_text
376
516
  class_name = self.class.name unless self.class.in_schema_classes
377
517
  class_txt = begin
378
518
  if class_name
@@ -384,7 +524,7 @@ module JSI
384
524
  "#{class_name} (#{schema_module_names.join(', ')})"
385
525
  end
386
526
  else
387
- schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name || schema.schema_id }.compact
527
+ schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name_from_ancestor || schema.schema_uri }.compact
388
528
  if schema_names.empty?
389
529
  "JSI"
390
530
  else
@@ -393,33 +533,87 @@ module JSI
393
533
  end
394
534
  end
395
535
 
396
- if (is_a?(PathedArrayNode) || is_a?(PathedHashNode)) && ![Array, Hash].include?(node_content.class)
397
- if node_content.respond_to?(:object_group_text)
398
- node_content_txt = node_content.object_group_text
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
399
539
  else
400
- node_content_txt = [node_content.class.to_s]
540
+ content_txt = [jsi_node_content.class.to_s]
401
541
  end
402
542
  else
403
- node_content_txt = []
543
+ content_txt = []
404
544
  end
405
545
 
406
546
  [
407
547
  class_txt,
408
548
  is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
409
- *node_content_txt,
549
+ *content_txt,
410
550
  ].compact
411
551
  end
412
552
 
413
- # @return [Object] a jsonifiable representation of the instance
553
+ # a jsonifiable representation of the instance
554
+ # @return [Object]
414
555
  def as_json(*opt)
415
556
  Typelike.as_json(jsi_instance, *opt)
416
557
  end
417
558
 
418
- # @return [Object] an opaque fingerprint of this JSI for FingerprintHash. JSIs are equal
419
- # if their instances are equal, and if the JSIs are of the same JSI class or subclass.
559
+ # an opaque fingerprint of this JSI for {Util::FingerprintHash}.
420
560
  def jsi_fingerprint
421
- {class: jsi_class, jsi_document: jsi_document, jsi_ptr: jsi_ptr}
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
+ }
422
570
  end
423
571
  include Util::FingerprintHash
572
+
573
+ private
574
+
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
587
+ end
588
+
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
599
+ end
600
+
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
611
+
612
+ if value_as_jsi
613
+ yield
614
+ else
615
+ value
616
+ end
617
+ end
424
618
  end
425
619
  end