jsi 0.4.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 +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