jsi 0.4.0 → 0.7.0

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