jsi 0.4.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/CHANGELOG.md +33 -0
- data/LICENSE.md +1 -1
- data/README.md +114 -42
- data/jsi.gemspec +14 -12
- data/lib/jsi/base/node.rb +183 -0
- data/lib/jsi/base.rb +388 -220
- data/lib/jsi/jsi_coder.rb +8 -7
- data/lib/jsi/metaschema.rb +0 -1
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +101 -0
- data/lib/jsi/metaschema_node.rb +159 -135
- data/lib/jsi/ptr.rb +303 -0
- data/lib/jsi/schema/application/child_application/contains.rb +25 -0
- data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
- data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
- data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
- data/lib/jsi/schema/application/child_application/items.rb +18 -0
- data/lib/jsi/schema/application/child_application/properties.rb +25 -0
- data/lib/jsi/schema/application/child_application.rb +38 -0
- data/lib/jsi/schema/application/draft04.rb +8 -0
- data/lib/jsi/schema/application/draft06.rb +8 -0
- data/lib/jsi/schema/application/draft07.rb +8 -0
- data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
- data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
- data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
- data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
- data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
- data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
- data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
- data/lib/jsi/schema/application/inplace_application.rb +41 -0
- data/lib/jsi/schema/application.rb +12 -0
- data/lib/jsi/schema/draft04.rb +14 -0
- data/lib/jsi/schema/draft06.rb +14 -0
- data/lib/jsi/schema/draft07.rb +14 -0
- data/lib/jsi/schema/issue.rb +36 -0
- data/lib/jsi/schema/ref.rb +160 -0
- data/lib/jsi/schema/schema_ancestor_node.rb +113 -0
- data/lib/jsi/schema/validation/array.rb +69 -0
- data/lib/jsi/schema/validation/const.rb +20 -0
- data/lib/jsi/schema/validation/contains.rb +25 -0
- data/lib/jsi/schema/validation/core.rb +39 -0
- data/lib/jsi/schema/validation/dependencies.rb +49 -0
- data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
- data/lib/jsi/schema/validation/draft04.rb +112 -0
- data/lib/jsi/schema/validation/draft06.rb +122 -0
- data/lib/jsi/schema/validation/draft07.rb +159 -0
- data/lib/jsi/schema/validation/enum.rb +25 -0
- data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
- data/lib/jsi/schema/validation/items.rb +54 -0
- data/lib/jsi/schema/validation/not.rb +20 -0
- data/lib/jsi/schema/validation/numeric.rb +121 -0
- data/lib/jsi/schema/validation/object.rb +45 -0
- data/lib/jsi/schema/validation/pattern.rb +34 -0
- data/lib/jsi/schema/validation/properties.rb +101 -0
- data/lib/jsi/schema/validation/property_names.rb +32 -0
- data/lib/jsi/schema/validation/ref.rb +40 -0
- data/lib/jsi/schema/validation/required.rb +27 -0
- data/lib/jsi/schema/validation/someof.rb +90 -0
- data/lib/jsi/schema/validation/string.rb +47 -0
- data/lib/jsi/schema/validation/type.rb +49 -0
- data/lib/jsi/schema/validation.rb +51 -0
- data/lib/jsi/schema.rb +508 -149
- data/lib/jsi/schema_classes.rb +199 -59
- data/lib/jsi/schema_registry.rb +151 -0
- data/lib/jsi/schema_set.rb +181 -0
- data/lib/jsi/simple_wrap.rb +23 -4
- data/lib/jsi/util/private/attr_struct.rb +127 -0
- data/lib/jsi/util/private.rb +204 -0
- data/lib/jsi/util/typelike.rb +229 -0
- data/lib/jsi/util.rb +89 -53
- data/lib/jsi/validation/error.rb +34 -0
- data/lib/jsi/validation/result.rb +210 -0
- data/lib/jsi/validation.rb +15 -0
- data/lib/jsi/version.rb +3 -1
- data/lib/jsi.rb +44 -14
- data/lib/schemas/json-schema.org/draft-04/schema.rb +10 -3
- data/lib/schemas/json-schema.org/draft-06/schema.rb +10 -3
- data/lib/schemas/json-schema.org/draft-07/schema.rb +14 -0
- data/readme.rb +138 -0
- data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
- data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
- data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
- metadata +75 -122
- data/.simplecov +0 -3
- data/Rakefile.rb +0 -9
- data/lib/jsi/base/to_rb.rb +0 -128
- data/lib/jsi/json/node.rb +0 -203
- data/lib/jsi/json/pointer.rb +0 -419
- data/lib/jsi/json-schema-fragments.rb +0 -61
- data/lib/jsi/json.rb +0 -10
- data/lib/jsi/pathed_node.rb +0 -118
- data/lib/jsi/typelike_modules.rb +0 -240
- data/resources/icons/AGPL-3.0.png +0 -0
- data/test/base_array_test.rb +0 -323
- data/test/base_hash_test.rb +0 -337
- data/test/base_test.rb +0 -486
- data/test/jsi_coder_test.rb +0 -85
- data/test/jsi_json_arraynode_test.rb +0 -150
- data/test/jsi_json_hashnode_test.rb +0 -132
- data/test/jsi_json_node_test.rb +0 -257
- data/test/jsi_json_pointer_test.rb +0 -102
- data/test/jsi_test.rb +0 -11
- data/test/jsi_typelike_as_json_test.rb +0 -53
- data/test/metaschema_node_test.rb +0 -19
- data/test/schema_module_test.rb +0 -21
- data/test/schema_test.rb +0 -208
- data/test/spreedly_openapi_test.rb +0 -8
- data/test/test_helper.rb +0 -97
- 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
|
4
|
+
# JSI::Base is the base class of every JSI instance of a JSON schema.
|
5
5
|
#
|
6
|
-
# a
|
7
|
-
#
|
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
|
-
#
|
10
|
-
#
|
11
|
-
#
|
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
|
-
|
15
|
-
|
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
|
-
#
|
21
|
-
#
|
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
|
-
#
|
36
|
-
#
|
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.
|
44
|
-
"#{mod.name} (#{schema.
|
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.
|
48
|
-
schema.
|
50
|
+
elsif schema.schema_uri
|
51
|
+
schema.schema_uri.to_s
|
49
52
|
else
|
50
|
-
schema.
|
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
|
-
# @
|
73
|
-
#
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
116
|
-
#
|
117
|
-
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
# @
|
121
|
-
#
|
122
|
-
#
|
123
|
-
# @param jsi_root_node [JSI::Base]
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
161
|
-
|
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
|
-
#
|
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::
|
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
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
187
|
-
|
188
|
-
|
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
|
-
#
|
191
|
-
#
|
192
|
-
|
193
|
-
|
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
|
282
|
+
def jsi_parent_nodes
|
200
283
|
parent = jsi_root_node
|
201
284
|
|
202
|
-
jsi_ptr.
|
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
|
213
|
-
|
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
|
-
|
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
|
-
#
|
219
|
-
|
220
|
-
# @
|
221
|
-
|
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
|
-
#
|
224
|
-
#
|
225
|
-
#
|
226
|
-
#
|
227
|
-
#
|
228
|
-
|
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 =
|
231
|
-
value =
|
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 =
|
234
|
-
value =
|
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
|
366
|
+
raise(CannotSubscriptError, "cannot subscript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
|
237
367
|
end
|
238
368
|
|
239
|
-
|
240
|
-
|
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
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
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
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
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(
|
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
|
-
#
|
298
|
-
#
|
299
|
-
|
300
|
-
|
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
|
312
|
-
# a modified copy of
|
313
|
-
# which will be used to instantiate a new
|
314
|
-
#
|
315
|
-
#
|
316
|
-
#
|
317
|
-
#
|
318
|
-
|
319
|
-
|
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
|
-
|
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.
|
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
|
-
|
447
|
+
modified_jsi_root_node.jsi_descendent_node(@jsi_ptr)
|
327
448
|
end
|
328
449
|
end
|
329
450
|
|
330
|
-
#
|
331
|
-
|
332
|
-
|
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
|
-
#
|
336
|
-
|
337
|
-
|
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
|
-
#
|
341
|
-
#
|
342
|
-
#
|
343
|
-
#
|
344
|
-
|
345
|
-
|
346
|
-
|
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
|
-
|
482
|
+
jsi_modified_copy(&:dup)
|
351
483
|
end
|
352
484
|
|
353
|
-
#
|
354
|
-
#
|
485
|
+
# a string representing this JSI, indicating any named schemas and inspecting its instance
|
486
|
+
# @return [String]
|
355
487
|
def inspect
|
356
|
-
"\#<#{
|
488
|
+
"\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
|
357
489
|
end
|
358
490
|
|
359
|
-
|
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
|
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
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
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?(
|
397
|
-
if
|
398
|
-
|
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
|
-
|
522
|
+
content_txt = [jsi_node_content.class.to_s]
|
401
523
|
end
|
402
524
|
else
|
403
|
-
|
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
|
-
*
|
531
|
+
*content_txt,
|
410
532
|
].compact
|
411
533
|
end
|
412
534
|
|
413
|
-
#
|
535
|
+
# a jsonifiable representation of the instance
|
536
|
+
# @return [Object]
|
414
537
|
def as_json(*opt)
|
415
|
-
|
538
|
+
Util.as_json(jsi_instance, *opt)
|
416
539
|
end
|
417
540
|
|
418
|
-
#
|
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
|
-
{
|
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
|