jsi 0.4.0 → 0.7.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +33 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +114 -42
  6. data/jsi.gemspec +14 -12
  7. data/lib/jsi/base/node.rb +183 -0
  8. data/lib/jsi/base.rb +388 -220
  9. data/lib/jsi/jsi_coder.rb +8 -7
  10. data/lib/jsi/metaschema.rb +0 -1
  11. data/lib/jsi/metaschema_node/bootstrap_schema.rb +101 -0
  12. data/lib/jsi/metaschema_node.rb +159 -135
  13. data/lib/jsi/ptr.rb +303 -0
  14. data/lib/jsi/schema/application/child_application/contains.rb +25 -0
  15. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  16. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  17. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  18. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  19. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  20. data/lib/jsi/schema/application/child_application.rb +38 -0
  21. data/lib/jsi/schema/application/draft04.rb +8 -0
  22. data/lib/jsi/schema/application/draft06.rb +8 -0
  23. data/lib/jsi/schema/application/draft07.rb +8 -0
  24. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  25. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  26. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  27. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  28. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  29. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  30. data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
  31. data/lib/jsi/schema/application/inplace_application.rb +41 -0
  32. data/lib/jsi/schema/application.rb +12 -0
  33. data/lib/jsi/schema/draft04.rb +14 -0
  34. data/lib/jsi/schema/draft06.rb +14 -0
  35. data/lib/jsi/schema/draft07.rb +14 -0
  36. data/lib/jsi/schema/issue.rb +36 -0
  37. data/lib/jsi/schema/ref.rb +160 -0
  38. data/lib/jsi/schema/schema_ancestor_node.rb +113 -0
  39. data/lib/jsi/schema/validation/array.rb +69 -0
  40. data/lib/jsi/schema/validation/const.rb +20 -0
  41. data/lib/jsi/schema/validation/contains.rb +25 -0
  42. data/lib/jsi/schema/validation/core.rb +39 -0
  43. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  44. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  45. data/lib/jsi/schema/validation/draft04.rb +112 -0
  46. data/lib/jsi/schema/validation/draft06.rb +122 -0
  47. data/lib/jsi/schema/validation/draft07.rb +159 -0
  48. data/lib/jsi/schema/validation/enum.rb +25 -0
  49. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  50. data/lib/jsi/schema/validation/items.rb +54 -0
  51. data/lib/jsi/schema/validation/not.rb +20 -0
  52. data/lib/jsi/schema/validation/numeric.rb +121 -0
  53. data/lib/jsi/schema/validation/object.rb +45 -0
  54. data/lib/jsi/schema/validation/pattern.rb +34 -0
  55. data/lib/jsi/schema/validation/properties.rb +101 -0
  56. data/lib/jsi/schema/validation/property_names.rb +32 -0
  57. data/lib/jsi/schema/validation/ref.rb +40 -0
  58. data/lib/jsi/schema/validation/required.rb +27 -0
  59. data/lib/jsi/schema/validation/someof.rb +90 -0
  60. data/lib/jsi/schema/validation/string.rb +47 -0
  61. data/lib/jsi/schema/validation/type.rb +49 -0
  62. data/lib/jsi/schema/validation.rb +51 -0
  63. data/lib/jsi/schema.rb +508 -149
  64. data/lib/jsi/schema_classes.rb +199 -59
  65. data/lib/jsi/schema_registry.rb +151 -0
  66. data/lib/jsi/schema_set.rb +181 -0
  67. data/lib/jsi/simple_wrap.rb +23 -4
  68. data/lib/jsi/util/private/attr_struct.rb +127 -0
  69. data/lib/jsi/util/private.rb +204 -0
  70. data/lib/jsi/util/typelike.rb +229 -0
  71. data/lib/jsi/util.rb +89 -53
  72. data/lib/jsi/validation/error.rb +34 -0
  73. data/lib/jsi/validation/result.rb +210 -0
  74. data/lib/jsi/validation.rb +15 -0
  75. data/lib/jsi/version.rb +3 -1
  76. data/lib/jsi.rb +44 -14
  77. data/lib/schemas/json-schema.org/draft-04/schema.rb +10 -3
  78. data/lib/schemas/json-schema.org/draft-06/schema.rb +10 -3
  79. data/lib/schemas/json-schema.org/draft-07/schema.rb +14 -0
  80. data/readme.rb +138 -0
  81. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  82. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  83. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  84. metadata +75 -122
  85. data/.simplecov +0 -3
  86. data/Rakefile.rb +0 -9
  87. data/lib/jsi/base/to_rb.rb +0 -128
  88. data/lib/jsi/json/node.rb +0 -203
  89. data/lib/jsi/json/pointer.rb +0 -419
  90. data/lib/jsi/json-schema-fragments.rb +0 -61
  91. data/lib/jsi/json.rb +0 -10
  92. data/lib/jsi/pathed_node.rb +0 -118
  93. data/lib/jsi/typelike_modules.rb +0 -240
  94. data/resources/icons/AGPL-3.0.png +0 -0
  95. data/test/base_array_test.rb +0 -323
  96. data/test/base_hash_test.rb +0 -337
  97. data/test/base_test.rb +0 -486
  98. data/test/jsi_coder_test.rb +0 -85
  99. data/test/jsi_json_arraynode_test.rb +0 -150
  100. data/test/jsi_json_hashnode_test.rb +0 -132
  101. data/test/jsi_json_node_test.rb +0 -257
  102. data/test/jsi_json_pointer_test.rb +0 -102
  103. data/test/jsi_test.rb +0 -11
  104. data/test/jsi_typelike_as_json_test.rb +0 -53
  105. data/test/metaschema_node_test.rb +0 -19
  106. data/test/schema_module_test.rb +0 -21
  107. data/test/schema_test.rb +0 -208
  108. data/test/spreedly_openapi_test.rb +0 -8
  109. data/test/test_helper.rb +0 -97
  110. data/test/util_test.rb +0 -62
data/lib/jsi/base.rb CHANGED
@@ -1,30 +1,32 @@
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
+ autoload :ArrayNode, 'jsi/base/node'
18
+ autoload :HashNode, 'jsi/base/node'
19
+
20
+ include Schema::SchemaAncestorNode
13
21
  include Util::Memoize
14
- include Enumerable
15
- include PathedNode
22
+
23
+ # an exception raised when {Base#[]} is invoked on an instance which is not an array or hash
16
24
  class CannotSubscriptError < StandardError
17
25
  end
18
26
 
19
27
  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)
25
- end
26
-
27
- # is the constant JSI::SchemaClasses::{self.schema_classes_const_name} defined?
28
+ # @private
29
+ # is the constant JSI::SchemaClasses::<self.schema_classes_const_name> defined?
28
30
  # (if so, we will prefer to use something more human-readable than that ugly mess.)
29
31
  def in_schema_classes
30
32
  # #name sets @in_schema_classes
@@ -32,22 +34,23 @@ module JSI
32
34
  @in_schema_classes
33
35
  end
34
36
 
35
- # @return [String] a string representing the class, indicating the schemas represented by their module
36
- # name or a URI
37
+ # a string indicating a class name if one is defined, as well as the schema module name
38
+ # and/or schema URI of each schema the class represents.
39
+ # @return [String]
37
40
  def inspect
38
41
  if !respond_to?(:jsi_class_schemas)
39
42
  super
40
43
  else
41
44
  schema_names = jsi_class_schemas.map do |schema|
42
45
  mod = schema.jsi_schema_module
43
- if mod.name && schema.schema_id
44
- "#{mod.name} (#{schema.schema_id})"
46
+ if mod.name && schema.schema_uri
47
+ "#{mod.name} (#{schema.schema_uri})"
45
48
  elsif mod.name
46
49
  mod.name
47
- elsif schema.schema_id
48
- schema.schema_id
50
+ elsif schema.schema_uri
51
+ schema.schema_uri.to_s
49
52
  else
50
- schema.node_ptr.uri
53
+ schema.jsi_ptr.uri.to_s
51
54
  end
52
55
  end
53
56
 
@@ -69,27 +72,33 @@ module JSI
69
72
 
70
73
  alias_method :to_s, :inspect
71
74
 
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.
75
+ # @private
76
+ # see {.name}
75
77
  def schema_classes_const_name
76
78
  if respond_to?(:jsi_class_schemas)
77
79
  schema_names = jsi_class_schemas.map do |schema|
78
- if schema.jsi_schema_module.name
79
- schema.jsi_schema_module.name
80
- elsif schema.schema_id
81
- schema.schema_id
80
+ named_ancestor_schema, tokens = schema.jsi_schema_module.send(:named_ancestor_schema_tokens)
81
+ if named_ancestor_schema
82
+ [named_ancestor_schema.jsi_schema_module.name, *tokens].join('_')
83
+ elsif schema.schema_uri
84
+ schema.schema_uri.to_s
82
85
  else
83
86
  nil
84
87
  end
85
88
  end
86
89
  if !schema_names.any?(&:nil?) && !schema_names.empty?
87
- schema_names.sort.map { |n| 'X' + n.gsub(/[^\w]/, '_') }.join('')
90
+ schema_names.sort.map { |n| 'X' + n.to_s.gsub(/[^\w]/, '_') }.join('')
88
91
  end
89
92
  end
90
93
  end
91
94
 
92
- # @return [String] a constant name of this class
95
+ # a constant name of this class. this is generated from the schema module name or URI of each schema
96
+ # this class represents. nil if any represented schema has no schema module name or schema URI.
97
+ #
98
+ # this generated name is not too pretty but can be more helpful than an anonymous class, especially
99
+ # in error messages.
100
+ #
101
+ # @return [String]
93
102
  def name
94
103
  unless instance_variable_defined?(:@in_schema_classes)
95
104
  const_name = schema_classes_const_name
@@ -104,104 +113,178 @@ module JSI
104
113
  end
105
114
  end
106
115
 
107
- # 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('::')))) }
110
-
111
- # initializes this JSI from the given instance - instance is most commonly
112
- # a parsed JSON document consisting of Hash, Array, or sometimes a basic
113
- # type, but this is in no way enforced and a JSI may wrap any object.
116
+ # initializes a JSI whose instance is in the given document at the given pointer.
114
117
  #
115
- # @param instance [Object] the JSON Schema instance being represented
116
- # @param jsi_document [Object] for internal use. the instance may be specified as a
117
- # node in the `jsi_document` param, pointed to by `jsi_ptr`. the param `instance`
118
- # MUST be `NOINSTANCE` to use the jsi_document + jsi_ptr form. `jsi_document` MUST
119
- # NOT be passed if `instance` is anything other than `NOINSTANCE`.
120
- # @param jsi_ptr [JSI::JSON::Pointer] for internal use. a JSON pointer specifying
121
- # the path of this instance in the `jsi_document` param. `jsi_ptr` must be passed
122
- # iff `jsi_document` is passed, i.e. when `instance` is `NOINSTANCE`
123
- # @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)
125
- unless respond_to?(:jsi_schemas)
126
- 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
- end
128
-
129
- if instance.is_a?(JSI::Schema)
130
- raise(TypeError, "assigning a schema to a #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
131
- elsif instance.is_a?(JSI::Base)
132
- raise(TypeError, "assigning another JSI::Base instance to a #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
133
- end
134
-
135
- 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
141
- if @jsi_ptr.root?
142
- raise(Bug, "jsi_root_node cannot be specified for root JSI") if jsi_root_node
143
- @jsi_root_node = self
144
- else
145
- if !jsi_root_node.is_a?(JSI::Base)
146
- raise(TypeError, "jsi_root_node must be a JSI::Base; got: #{jsi_root_node.inspect}")
147
- end
148
- if !jsi_root_node.jsi_ptr.root?
149
- raise(Bug, "jsi_root_node ptr #{jsi_root_node.jsi_ptr.inspect} is not root")
150
- end
151
- @jsi_root_node = jsi_root_node
152
- end
153
- else
154
- raise(Bug, 'incorrect usage') if jsi_document || jsi_ptr || jsi_root_node
155
- @jsi_document = instance
156
- @jsi_ptr = JSI::JSON::Pointer[]
118
+ # this is a private api - users should look elsewhere to instantiate JSIs, in particular:
119
+ #
120
+ # - {JSI.new_schema} and {Schema::DescribesSchema#new_schema} to instantiate schemas
121
+ # - {Schema#new_jsi} to instantiate schema instances
122
+ #
123
+ # @api private
124
+ # @param jsi_document [Object] the document containing the instance
125
+ # @param jsi_ptr [JSI::Ptr] a pointer pointing to the JSI's instance in the document
126
+ # @param jsi_root_node [JSI::Base] the JSI of the root of the document containing this JSI
127
+ # @param jsi_schema_base_uri [Addressable::URI] see {SchemaSet#new_jsi} param uri
128
+ # @param jsi_schema_resource_ancestors [Array<JSI::Base<JSI::Schema>>]
129
+ def initialize(jsi_document,
130
+ jsi_ptr: Ptr[],
131
+ jsi_root_node: nil,
132
+ jsi_schema_base_uri: nil,
133
+ jsi_schema_resource_ancestors: Util::EMPTY_ARY
134
+ )
135
+ raise(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)
136
+
137
+ jsi_initialize_memos
138
+
139
+ self.jsi_document = jsi_document
140
+ self.jsi_ptr = jsi_ptr
141
+ if @jsi_ptr.root?
142
+ raise(Bug, "jsi_root_node specified for root JSI") if jsi_root_node
157
143
  @jsi_root_node = self
144
+ else
145
+ raise(Bug, "jsi_root_node is not JSI::Base") if !jsi_root_node.is_a?(JSI::Base)
146
+ raise(Bug, "jsi_root_node ptr is not root") if !jsi_root_node.jsi_ptr.root?
147
+ @jsi_root_node = jsi_root_node
158
148
  end
149
+ self.jsi_schema_base_uri = jsi_schema_base_uri
150
+ self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors
159
151
 
160
- if self.jsi_instance.respond_to?(:to_hash)
161
- extend PathedHashNode
162
- elsif self.jsi_instance.respond_to?(:to_ary)
163
- extend PathedArrayNode
164
- end
165
-
166
- jsi_schemas.each do |schema|
167
- if schema.describes_schema?
168
- extend JSI::Schema
169
- end
152
+ if jsi_instance.is_a?(JSI::Base)
153
+ raise(TypeError, "a JSI::Base instance must not be another JSI::Base. received: #{jsi_instance.pretty_inspect.chomp}")
170
154
  end
171
155
  end
172
156
 
173
- # document containing the instance of this JSI
157
+ # @!method jsi_schemas
158
+ # the set of schemas which describe this instance
159
+ # @return [JSI::SchemaSet]
160
+ # note: defined on subclasses by JSI::SchemaClasses.class_for_schemas
161
+
162
+
163
+ # document containing the instance of this JSI at our {#jsi_ptr}
174
164
  attr_reader :jsi_document
175
165
 
176
- # JSI::JSON::Pointer pointing to this JSI's instance within the jsi_document
166
+ # {JSI::Ptr} pointing to this JSI's instance within our {#jsi_document}
167
+ # @return [JSI::Ptr]
177
168
  attr_reader :jsi_ptr
178
169
 
179
170
  # the JSI at the root of this JSI's document
171
+ # @return [JSI::Base]
180
172
  attr_reader :jsi_root_node
181
173
 
182
- alias_method :node_document, :jsi_document
183
- alias_method :node_ptr, :jsi_ptr
184
- alias_method :document_root_node, :jsi_root_node
174
+ # the content of this node in our {#jsi_document} at our {#jsi_ptr}. the same as {#jsi_instance}.
175
+ def jsi_node_content
176
+ content = jsi_ptr.evaluate(jsi_document)
177
+ content
178
+ end
179
+
180
+ # the JSON schema instance this JSI represents - the underlying JSON data used to instantiate this JSI
181
+ alias_method :jsi_instance, :jsi_node_content
182
+
183
+ # yields a JSI of each node at or below this one in this JSI's document.
184
+ #
185
+ # returns an Enumerator if no block is given.
186
+ #
187
+ # @yield [JSI::Base] each descendent node, starting with self
188
+ # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
189
+ def jsi_each_descendent_node(&block)
190
+ return to_enum(__method__) unless block
185
191
 
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
192
+ yield self
193
+ if respond_to?(:to_hash)
194
+ each_key do |k|
195
+ self[k, as_jsi: true].jsi_each_descendent_node(&block)
196
+ end
197
+ elsif respond_to?(:to_ary)
198
+ each_index do |i|
199
+ self[i, as_jsi: true].jsi_each_descendent_node(&block)
200
+ end
201
+ end
202
+ nil
203
+ end
189
204
 
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
193
- raise NoMethodError, "Enumerable methods and #each not implemented for instance that is not like a hash or array: #{jsi_instance.pretty_inspect.chomp}"
205
+ # recursively selects descendent nodes of this JSI, returning a modified copy of self containing only
206
+ # descendent nodes for which the given block had a true-ish result.
207
+ #
208
+ # this method yields a node before recursively descending to its child nodes, so leaf nodes are yielded
209
+ # last, after their parents. if a node is not selected, its descendents are never recursed.
210
+ #
211
+ # @yield [JSI::Base] each descendent node below self
212
+ # @return [JSI::Base] modified copy of self containing only the selected nodes
213
+ def jsi_select_descendents_node_first(&block)
214
+ jsi_modified_copy do |instance|
215
+ if respond_to?(:to_hash)
216
+ res = instance.class.new
217
+ each_key do |k|
218
+ v = self[k, as_jsi: true]
219
+ if yield(v)
220
+ res[k] = v.jsi_select_descendents_node_first(&block).jsi_node_content
221
+ end
222
+ end
223
+ res
224
+ elsif respond_to?(:to_ary)
225
+ res = instance.class.new
226
+ each_index do |i|
227
+ e = self[i, as_jsi: true]
228
+ if yield(e)
229
+ res << e.jsi_select_descendents_node_first(&block).jsi_node_content
230
+ end
231
+ end
232
+ res
233
+ else
234
+ instance
235
+ end
236
+ end
237
+ end
238
+
239
+ # @deprecated after v0.6
240
+ alias_method :jsi_select_children_node_first, :jsi_select_descendents_node_first
241
+
242
+ # recursively selects descendent nodes of this JSI, returning a modified copy of self containing only
243
+ # descendent nodes for which the given block had a true-ish result.
244
+ #
245
+ # this method recursively descends child nodes before yielding each node, so leaf nodes are yielded
246
+ # before their parents.
247
+ #
248
+ # @yield [JSI::Base] each descendent node below self
249
+ # @return [JSI::Base] modified copy of self containing only the selected nodes
250
+ def jsi_select_descendents_leaf_first(&block)
251
+ jsi_modified_copy do |instance|
252
+ if respond_to?(:to_hash)
253
+ res = instance.class.new
254
+ each_key do |k|
255
+ v = self[k, as_jsi: true].jsi_select_descendents_leaf_first(&block)
256
+ if yield(v)
257
+ res[k] = v.jsi_node_content
258
+ end
259
+ end
260
+ res
261
+ elsif respond_to?(:to_ary)
262
+ res = instance.class.new
263
+ each_index do |i|
264
+ e = self[i, as_jsi: true].jsi_select_descendents_leaf_first(&block)
265
+ if yield(e)
266
+ res << e.jsi_node_content
267
+ end
268
+ end
269
+ res
270
+ else
271
+ instance
272
+ end
273
+ end
194
274
  end
195
275
 
276
+ # @deprecated after v0.6
277
+ alias_method :jsi_select_children_leaf_first, :jsi_select_descendents_leaf_first
278
+
196
279
  # an array of JSI instances above this one in the document.
197
280
  #
198
281
  # @return [Array<JSI::Base>]
199
- def parent_jsis
282
+ def jsi_parent_nodes
200
283
  parent = jsi_root_node
201
284
 
202
- jsi_ptr.reference_tokens.map do |token|
285
+ jsi_ptr.tokens.map do |token|
203
286
  parent.tap do
204
- parent = parent[token]
287
+ parent = parent[token, as_jsi: true]
205
288
  end
206
289
  end.reverse
207
290
  end
@@ -209,62 +292,105 @@ module JSI
209
292
  # the immediate parent of this JSI. nil if there is no parent.
210
293
  #
211
294
  # @return [JSI::Base, nil]
212
- def parent_jsi
213
- parent_jsis.first
295
+ def jsi_parent_node
296
+ jsi_ptr.root? ? nil : jsi_root_node.jsi_descendent_node(jsi_ptr.parent)
214
297
  end
215
298
 
216
- alias_method :parent_node, :parent_jsi
299
+ # ancestor JSI instances from this node up to the root. this node itself is always its own first ancestor.
300
+ #
301
+ # @return [Array<JSI::Base>]
302
+ def jsi_ancestor_nodes
303
+ ancestors = []
304
+ ancestor = jsi_root_node
305
+ ancestors << ancestor
306
+
307
+ jsi_ptr.tokens.each do |token|
308
+ ancestor = ancestor[token, as_jsi: true]
309
+ ancestors << ancestor
310
+ end
311
+ ancestors.reverse!.freeze
312
+ end
217
313
 
218
- # @deprecated
219
- alias_method :parents, :parent_jsis
220
- # @deprecated
221
- alias_method :parent, :parent_jsi
314
+ # the descendent node at the given pointer
315
+ #
316
+ # @param ptr [JSI::Ptr, #to_ary]
317
+ # @return [JSI::Base]
318
+ def jsi_descendent_node(ptr)
319
+ descendent = Ptr.ary_ptr(ptr).evaluate(self, as_jsi: true)
320
+ descendent
321
+ end
222
322
 
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)
323
+ # subscripts to return a child value identified by the given token.
324
+ #
325
+ # @param token [String, Integer, Object] an array index or hash key (JSON object property name)
326
+ # of the instance identifying the child value
327
+ # @param as_jsi [:auto, true, false] whether to return the result value as a JSI. one of:
328
+ #
329
+ # - :auto (default): by default a JSI will be returned when either:
330
+ #
331
+ # - the result is a complex value (responds to #to_ary or #to_hash)
332
+ # - the result is a schema (including true/false schemas)
333
+ #
334
+ # a plain value is returned when no schemas are known to describe the instance, or when the value is a
335
+ # simple type (anything unresponsive to #to_ary / #to_hash).
336
+ #
337
+ # - true: the result value will always be returned as a JSI. the {#jsi_schemas} of the result may be
338
+ # empty if no schemas describe the instance.
339
+ # - false: the result value will always be the plain instance.
340
+ #
341
+ # note that nil is returned (regardless of as_jsi) when there is no value to return because the token
342
+ # is not a hash key or array index of the instance and no default value applies.
343
+ # (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
344
+ # unspecified behavior.)
345
+ # @param use_default [true, false] whether to return a schema default value when the token is not in
346
+ # range. if the token is not an array index or hash key of the instance, and one schema for the child
347
+ # instance specifies a default value, that default is returned.
348
+ #
349
+ # if the result with the default value is a JSI (per the `as_jsi` param), that JSI is not a child of
350
+ # this JSI - this JSI is not modified to fill in the default value. the result is a JSI within a new
351
+ # document containing the filled-in default.
352
+ #
353
+ # if the child instance's schemas do not indicate a single default value (that is, if zero or multiple
354
+ # defaults are specified across those schemas), nil is returned.
355
+ # (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
356
+ # unspecified behavior.)
357
+ # @return [JSI::Base, Object] the child value identified by the subscript token
358
+ def [](token, as_jsi: :auto, use_default: true)
229
359
  if respond_to?(:to_hash)
230
- token_in_range = node_content_hash_pubsend(:key?, token)
231
- value = node_content_hash_pubsend(:[], token)
360
+ token_in_range = jsi_node_content_hash_pubsend(:key?, token)
361
+ value = jsi_node_content_hash_pubsend(:[], token)
232
362
  elsif respond_to?(:to_ary)
233
- token_in_range = node_content_ary_pubsend(:each_index).include?(token)
234
- value = node_content_ary_pubsend(:[], token)
363
+ token_in_range = jsi_node_content_ary_pubsend(:each_index).include?(token)
364
+ value = jsi_node_content_ary_pubsend(:[], token)
235
365
  else
236
- raise(CannotSubscriptError, "cannot subcript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
366
+ raise(CannotSubscriptError, "cannot subscript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
237
367
  end
238
368
 
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, &:|)
369
+ begin
370
+ subinstance_schemas = jsi_subinstance_schemas_memos[token: token, instance: jsi_node_content, subinstance: value]
246
371
 
247
372
  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
373
+ jsi_subinstance_as_jsi(value, subinstance_schemas, as_jsi) do
374
+ jsi_subinstance_memos[
375
+ token: token,
376
+ subinstance_schemas: subinstance_schemas,
377
+ includes: SchemaClasses.includes_for(value),
378
+ ]
255
379
  end
256
380
  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']
381
+ if use_default
382
+ defaults = Set.new
383
+ subinstance_schemas.each do |subinstance_schema|
384
+ if subinstance_schema.keyword?('default')
385
+ defaults << subinstance_schema.jsi_node_content['default']
386
+ end
261
387
  end
262
388
  end
263
389
 
264
- if defaults.size == 1
390
+ if use_default && defaults.size == 1
265
391
  # use the default value
266
392
  # 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]
393
+ dup.tap { |o| o[token] = defaults.first }[token, as_jsi: as_jsi]
268
394
  else
269
395
  # I kind of want to just return nil here. the preferred mechanism for
270
396
  # a JSI's default value should be its schema. but returning nil ignores
@@ -274,7 +400,6 @@ module JSI
274
400
  end
275
401
  end
276
402
  end
277
- result
278
403
  end
279
404
 
280
405
  # assigns the subscript of the instance identified by the given token to the given value.
@@ -284,9 +409,8 @@ module JSI
284
409
  # @param value [JSI::Base, Object] the value to be assigned
285
410
  def []=(token, value)
286
411
  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}")
412
+ raise(CannotSubscriptError, "cannot assign subscript (using token: #{token.inspect}) to instance: #{jsi_instance.pretty_inspect.chomp}")
288
413
  end
289
- jsi_clear_memo(:[])
290
414
  if value.is_a?(Base)
291
415
  self[token] = value.jsi_instance
292
416
  else
@@ -294,73 +418,83 @@ module JSI
294
418
  end
295
419
  end
296
420
 
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
421
+ # the set of JSI schema modules corresponding to the schemas that describe this JSI
422
+ # @return [Set<Module>]
423
+ def jsi_schema_modules
424
+ Util.ensure_module_set(jsi_schemas.map(&:jsi_schema_module))
309
425
  end
310
426
 
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?
427
+ # yields the content of this JSI's instance. the block must result in
428
+ # a modified copy of the yielded instance (not modified in place, which would alter this JSI
429
+ # as well) which will be used to instantiate and return a new JSI with the modified content.
430
+ #
431
+ # the result may have different schemas which describe it than this JSI's schemas,
432
+ # if conditional applicator schemas apply differently to the modified instance.
433
+ #
434
+ # @yield [Object] this JSI's instance. the block should result
435
+ # in a nondestructively modified copy of this.
436
+ # @return [JSI::Base subclass] the modified copy of self
437
+ def jsi_modified_copy(&block)
438
+ if @jsi_ptr.root?
320
439
  modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
321
- self.class.new(Base::NOINSTANCE, jsi_document: modified_document, jsi_ptr: @jsi_ptr)
440
+ jsi_schemas.new_jsi(modified_document,
441
+ uri: jsi_schema_base_uri,
442
+ )
322
443
  else
323
- modified_jsi_root_node = @jsi_root_node.modified_copy do |root|
444
+ modified_jsi_root_node = @jsi_root_node.jsi_modified_copy do |root|
324
445
  @jsi_ptr.modified_document_copy(root, &block)
325
446
  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)
447
+ modified_jsi_root_node.jsi_descendent_node(@jsi_ptr)
327
448
  end
328
449
  end
329
450
 
330
- # @return [Array] array of schema validation errors for this instance
331
- 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([], &:+)
451
+ # validates this JSI's instance against its schemas
452
+ #
453
+ # @return [JSI::Validation::FullResult]
454
+ def jsi_validate
455
+ jsi_schemas.instance_validate(self)
333
456
  end
334
457
 
335
- # @return [true, false] whether the instance validates against its schema
336
- def validate
337
- jsi_schemas.all? { |schema| schema.validate_instance(jsi_instance) }
458
+ # whether this JSI's instance is valid against all of its schemas
459
+ # @return [Boolean]
460
+ def jsi_valid?
461
+ jsi_schemas.instance_valid?(self)
338
462
  end
339
463
 
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
344
- def validate!
345
- jsi_schemas.each { |schema| schema.validate_instance!(jsi_instance) }
346
- true
464
+ # queries this JSI using the [JMESPath Ruby](https://rubygems.org/gems/jmespath) gem.
465
+ # see [https://jmespath.org/](https://jmespath.org/) to learn the JMESPath query language.
466
+ #
467
+ # the JMESPath gem is not a dependency of JSI, so must be installed / added to your Gemfile to use.
468
+ # e.g. `gem 'jmespath', '~> 1.5'`. note that versions below 1.5 are not compatible with JSI.
469
+ #
470
+ # @param expression [String] a [JMESPath](https://jmespath.org/) expression
471
+ # @param runtime_options passed to [JMESPath.search](https://rubydoc.info/gems/jmespath/JMESPath#search-class_method),
472
+ # though no runtime_options are publicly documented or normally used.
473
+ # @return [Array, Object, nil] query results.
474
+ # see [JMESPath.search](https://rubydoc.info/gems/jmespath/JMESPath#search-class_method)
475
+ def jmespath_search(expression, **runtime_options)
476
+ Util.require_jmespath
477
+
478
+ JMESPath.search(expression, self, **runtime_options)
347
479
  end
348
480
 
349
481
  def dup
350
- modified_copy(&:dup)
482
+ jsi_modified_copy(&:dup)
351
483
  end
352
484
 
353
- # @return [String] a string representing this JSI, indicating its class
354
- # and inspecting its instance
485
+ # a string representing this JSI, indicating any named schemas and inspecting its instance
486
+ # @return [String]
355
487
  def inspect
356
- "\#<#{object_group_text.join(' ')} #{jsi_instance.inspect}>"
488
+ "\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
357
489
  end
358
490
 
359
- # pretty-prints a representation this JSI to the given printer
491
+ alias_method :to_s, :inspect
492
+
493
+ # pretty-prints a representation of this JSI to the given printer
360
494
  # @return [void]
361
495
  def pretty_print(q)
362
496
  q.text '#<'
363
- q.text object_group_text.join(' ')
497
+ q.text jsi_object_group_text.join(' ')
364
498
  q.group_sub {
365
499
  q.nest(2) {
366
500
  q.breakable ' '
@@ -371,55 +505,89 @@ module JSI
371
505
  q.text '>'
372
506
  end
373
507
 
508
+ # @private
374
509
  # @return [Array<String>]
375
- def object_group_text
376
- class_name = self.class.name unless self.class.in_schema_classes
377
- class_txt = begin
378
- if class_name
379
- # ignore ID
380
- schema_module_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name }.compact
381
- if schema_module_names.empty?
382
- class_name
383
- else
384
- "#{class_name} (#{schema_module_names.join(', ')})"
385
- end
386
- else
387
- schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name || schema.schema_id }.compact
388
- if schema_names.empty?
389
- "JSI"
390
- else
391
- "JSI (#{schema_names.join(', ')})"
392
- end
393
- end
510
+ def jsi_object_group_text
511
+ schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name_from_ancestor || schema.schema_uri }.compact
512
+ if schema_names.empty?
513
+ class_txt = "JSI"
514
+ else
515
+ class_txt = "JSI (#{schema_names.join(', ')})"
394
516
  end
395
517
 
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
518
+ if (is_a?(ArrayNode) || is_a?(HashNode)) && ![Array, Hash].include?(jsi_node_content.class)
519
+ if jsi_node_content.respond_to?(:jsi_object_group_text)
520
+ content_txt = jsi_node_content.jsi_object_group_text
399
521
  else
400
- node_content_txt = [node_content.class.to_s]
522
+ content_txt = [jsi_node_content.class.to_s]
401
523
  end
402
524
  else
403
- node_content_txt = []
525
+ content_txt = []
404
526
  end
405
527
 
406
528
  [
407
529
  class_txt,
408
530
  is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
409
- *node_content_txt,
531
+ *content_txt,
410
532
  ].compact
411
533
  end
412
534
 
413
- # @return [Object] a jsonifiable representation of the instance
535
+ # a jsonifiable representation of the instance
536
+ # @return [Object]
414
537
  def as_json(*opt)
415
- Typelike.as_json(jsi_instance, *opt)
538
+ Util.as_json(jsi_instance, *opt)
416
539
  end
417
540
 
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.
541
+ # an opaque fingerprint of this JSI for {Util::FingerprintHash}.
420
542
  def jsi_fingerprint
421
- {class: jsi_class, jsi_document: jsi_document, jsi_ptr: jsi_ptr}
543
+ {
544
+ class: jsi_class,
545
+ jsi_document: jsi_document,
546
+ jsi_ptr: jsi_ptr,
547
+ # for instances in documents with schemas:
548
+ jsi_resource_ancestor_uri: jsi_resource_ancestor_uri,
549
+ # only defined for JSI::Schema instances:
550
+ jsi_schema_instance_modules: is_a?(Schema) ? jsi_schema_instance_modules : nil,
551
+ }
422
552
  end
423
553
  include Util::FingerprintHash
554
+
555
+ private
556
+
557
+ def jsi_subinstance_schemas_memos
558
+ jsi_memomap(:subinstance_schemas, key_by: -> (i) { i[:token] }) do |token: , instance: , subinstance: |
559
+ jsi_schemas.child_applicator_schemas(token, instance).inplace_applicator_schemas(subinstance)
560
+ end
561
+ end
562
+
563
+ def jsi_subinstance_memos
564
+ jsi_memomap(:subinstance, key_by: -> (i) { i[:token] }) do |token: , subinstance_schemas: , includes: |
565
+ jsi_class = JSI::SchemaClasses.class_for_schemas(subinstance_schemas, includes: includes)
566
+ jsi_class.new(@jsi_document,
567
+ jsi_ptr: @jsi_ptr[token],
568
+ jsi_root_node: @jsi_root_node,
569
+ jsi_schema_base_uri: jsi_resource_ancestor_uri,
570
+ jsi_schema_resource_ancestors: is_a?(Schema) ? jsi_subschema_resource_ancestors : jsi_schema_resource_ancestors,
571
+ )
572
+ end
573
+ end
574
+
575
+ def jsi_subinstance_as_jsi(value, subinstance_schemas, as_jsi)
576
+ if [true, false].include?(as_jsi)
577
+ value_as_jsi = as_jsi
578
+ elsif as_jsi == :auto
579
+ complex_value = value.respond_to?(:to_hash) || value.respond_to?(:to_ary)
580
+ schema_value = subinstance_schemas.any?(&:describes_schema?)
581
+ value_as_jsi = complex_value || schema_value
582
+ else
583
+ raise(ArgumentError, "as_jsi must be one of: :auto, true, false")
584
+ end
585
+
586
+ if value_as_jsi
587
+ yield
588
+ else
589
+ value
590
+ end
591
+ end
424
592
  end
425
593
  end