jsi-dev 0.0.0.pre.kramdown
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.
- checksums.yaml +7 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +101 -0
- data/LICENSE.md +613 -0
- data/README.md +303 -0
- data/docs/glossary.md +281 -0
- data/jsi.gemspec +30 -0
- data/lib/jsi/base/node.rb +373 -0
- data/lib/jsi/base.rb +738 -0
- data/lib/jsi/jsi_coder.rb +92 -0
- data/lib/jsi/metaschema.rb +6 -0
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +126 -0
- data/lib/jsi/metaschema_node.rb +262 -0
- data/lib/jsi/ptr.rb +314 -0
- data/lib/jsi/schema/application/child_application/contains.rb +25 -0
- data/lib/jsi/schema/application/child_application/draft04.rb +21 -0
- data/lib/jsi/schema/application/child_application/draft06.rb +28 -0
- data/lib/jsi/schema/application/child_application/draft07.rb +28 -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 +13 -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 +25 -0
- data/lib/jsi/schema/application/inplace_application/draft06.rb +26 -0
- data/lib/jsi/schema/application/inplace_application/draft07.rb +32 -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 +14 -0
- data/lib/jsi/schema/application.rb +12 -0
- data/lib/jsi/schema/draft04.rb +13 -0
- data/lib/jsi/schema/draft06.rb +13 -0
- data/lib/jsi/schema/draft07.rb +13 -0
- data/lib/jsi/schema/issue.rb +36 -0
- data/lib/jsi/schema/ref.rb +183 -0
- data/lib/jsi/schema/schema_ancestor_node.rb +122 -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/dependencies.rb +49 -0
- data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
- data/lib/jsi/schema/validation/draft04.rb +110 -0
- data/lib/jsi/schema/validation/draft06.rb +120 -0
- data/lib/jsi/schema/validation/draft07.rb +157 -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 +49 -0
- data/lib/jsi/schema.rb +792 -0
- data/lib/jsi/schema_classes.rb +357 -0
- data/lib/jsi/schema_registry.rb +190 -0
- data/lib/jsi/schema_set.rb +219 -0
- data/lib/jsi/simple_wrap.rb +26 -0
- data/lib/jsi/util/private/attr_struct.rb +130 -0
- data/lib/jsi/util/private/memo_map.rb +75 -0
- data/lib/jsi/util/private.rb +202 -0
- data/lib/jsi/util/typelike.rb +225 -0
- data/lib/jsi/util.rb +227 -0
- data/lib/jsi/validation/error.rb +34 -0
- data/lib/jsi/validation/result.rb +212 -0
- data/lib/jsi/validation.rb +15 -0
- data/lib/jsi/version.rb +5 -0
- data/lib/jsi.rb +105 -0
- data/lib/schemas/json-schema.org/draft-04/schema.rb +169 -0
- data/lib/schemas/json-schema.org/draft-06/schema.rb +171 -0
- data/lib/schemas/json-schema.org/draft-07/schema.rb +198 -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 +155 -0
data/lib/jsi/base.rb
ADDED
@@ -0,0 +1,738 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
# A JSI::Base instance represents a node in a JSON document (its {#jsi_document}) at a particular
|
5
|
+
# location (its {#jsi_ptr}), described by any number of JSON Schemas (its {#jsi_schemas}).
|
6
|
+
#
|
7
|
+
# JSI::Base is an abstract base class. The subclasses used to instantiate JSIs are dynamically created as
|
8
|
+
# needed for a given instance.
|
9
|
+
#
|
10
|
+
# These subclasses are generally intended to be ignored by applications using this library - the purpose
|
11
|
+
# they serve is to include modules relevant to the instance. The modules these classes include are:
|
12
|
+
#
|
13
|
+
# - the {Schema#jsi_schema_module} of each schema which describes the instance
|
14
|
+
# - {Base::HashNode}, {Base::ArrayNode}, or {Base::StringNode} if the instance is
|
15
|
+
# a hash/object, array, or string
|
16
|
+
# - Modules defining accessor methods for property names described by the schemas
|
17
|
+
class Base
|
18
|
+
autoload :ArrayNode, 'jsi/base/node'
|
19
|
+
autoload :HashNode, 'jsi/base/node'
|
20
|
+
autoload :StringNode, 'jsi/base/node'
|
21
|
+
|
22
|
+
include Schema::SchemaAncestorNode
|
23
|
+
|
24
|
+
# An exception raised when attempting to access a child of a node which cannot have children.
|
25
|
+
# A complex node can have children, a simple node cannot.
|
26
|
+
class SimpleNodeChildError < StandardError
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
# A string indicating the schema module name
|
31
|
+
# and/or schema URI of each schema the class represents.
|
32
|
+
# @return [String]
|
33
|
+
def inspect
|
34
|
+
if !respond_to?(:jsi_class_schemas)
|
35
|
+
super
|
36
|
+
else
|
37
|
+
schema_names = jsi_class_schemas.map do |schema|
|
38
|
+
mod_name = schema.jsi_schema_module.name_from_ancestor
|
39
|
+
if mod_name && schema.schema_absolute_uri
|
40
|
+
"#{mod_name} <#{schema.schema_absolute_uri}>"
|
41
|
+
elsif mod_name
|
42
|
+
mod_name
|
43
|
+
elsif schema.schema_uri
|
44
|
+
schema.schema_uri.to_s
|
45
|
+
else
|
46
|
+
schema.jsi_ptr.uri.to_s
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
if schema_names.empty?
|
51
|
+
"(JSI Schema Class for 0 schemas)"
|
52
|
+
else
|
53
|
+
-"(JSI Schema Class: #{schema_names.join(' + ')})"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
alias_method :to_s, :inspect
|
59
|
+
# A constant name of this class. This is generated from any schema module name or URI of each schema
|
60
|
+
# this class represents, or random characters.
|
61
|
+
#
|
62
|
+
# this generated name is not too pretty but can be more helpful than an anonymous class, especially
|
63
|
+
# in error messages.
|
64
|
+
#
|
65
|
+
# @return [String]
|
66
|
+
def name
|
67
|
+
return super if instance_variable_defined?(:@tried_to_name)
|
68
|
+
@tried_to_name = true
|
69
|
+
return super unless respond_to?(:jsi_class_schemas)
|
70
|
+
alnum = proc { |id| (id % 36**4).to_s(36).rjust(4, '0').upcase }
|
71
|
+
schema_names = jsi_class_schemas.map do |schema|
|
72
|
+
named_ancestor_schema, tokens = schema.jsi_schema_module.send(:named_ancestor_schema_tokens)
|
73
|
+
if named_ancestor_schema
|
74
|
+
[named_ancestor_schema.jsi_schema_module.name, *tokens].join('_')
|
75
|
+
elsif schema.schema_uri
|
76
|
+
schema.schema_uri.to_s
|
77
|
+
else
|
78
|
+
[alnum[schema.jsi_root_node.__id__], *schema.jsi_ptr.tokens].join('_')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
includes_names = jsi_class_includes.map { |m| m.name.sub(/\AJSI::Base::/, '').gsub(Util::RUBY_REJECT_NAME_RE, '_') }
|
82
|
+
if schema_names.any?
|
83
|
+
parts = schema_names.compact.sort.map { |n| 'X' + n.to_s }
|
84
|
+
parts += includes_names
|
85
|
+
const_name = Util.const_name_from_parts(parts, join: '__')
|
86
|
+
const_name += "__" + alnum[__id__] if SchemaClasses.const_defined?(const_name)
|
87
|
+
else
|
88
|
+
const_name = (['X' + alnum[__id__]] + includes_names).join('__')
|
89
|
+
end
|
90
|
+
# collisions are technically possible though vanishingly unlikely
|
91
|
+
SchemaClasses.const_set(const_name, self) unless SchemaClasses.const_defined?(const_name)
|
92
|
+
super
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# initializes a JSI whose instance is in the given document at the given pointer.
|
97
|
+
#
|
98
|
+
# this is a private api - users should look elsewhere to instantiate JSIs, in particular:
|
99
|
+
#
|
100
|
+
# - {JSI.new_schema} and {Schema::DescribesSchema#new_schema} to instantiate schemas
|
101
|
+
# - {Schema#new_jsi} to instantiate schema instances
|
102
|
+
#
|
103
|
+
# @api private
|
104
|
+
# @param jsi_document [Object] the document containing the instance
|
105
|
+
# @param jsi_ptr [JSI::Ptr] a pointer pointing to the JSI's instance in the document
|
106
|
+
# @param jsi_schema_base_uri [Addressable::URI] see {SchemaSet#new_jsi} param uri
|
107
|
+
# @param jsi_schema_resource_ancestors [Array<JSI::Base + JSI::Schema>]
|
108
|
+
# @param jsi_root_node [JSI::Base] the JSI of the root of the document containing this JSI
|
109
|
+
def initialize(jsi_document,
|
110
|
+
jsi_ptr: Ptr[],
|
111
|
+
jsi_indicated_schemas: ,
|
112
|
+
jsi_schema_base_uri: nil,
|
113
|
+
jsi_schema_resource_ancestors: Util::EMPTY_ARY,
|
114
|
+
jsi_schema_registry: ,
|
115
|
+
jsi_root_node: nil
|
116
|
+
)
|
117
|
+
raise(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)
|
118
|
+
|
119
|
+
super()
|
120
|
+
|
121
|
+
self.jsi_document = jsi_document
|
122
|
+
self.jsi_ptr = jsi_ptr
|
123
|
+
self.jsi_indicated_schemas = jsi_indicated_schemas
|
124
|
+
self.jsi_schema_base_uri = jsi_schema_base_uri
|
125
|
+
self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors
|
126
|
+
self.jsi_schema_registry = jsi_schema_registry
|
127
|
+
if @jsi_ptr.root?
|
128
|
+
raise(Bug, "jsi_root_node specified for root JSI") if jsi_root_node
|
129
|
+
@jsi_root_node = self
|
130
|
+
else
|
131
|
+
raise(Bug, "jsi_root_node is not JSI::Base") if !jsi_root_node.is_a?(JSI::Base)
|
132
|
+
raise(Bug, "jsi_root_node ptr is not root") if !jsi_root_node.jsi_ptr.root?
|
133
|
+
@jsi_root_node = jsi_root_node
|
134
|
+
end
|
135
|
+
|
136
|
+
jsi_memomaps_initialize
|
137
|
+
|
138
|
+
if jsi_instance.is_a?(JSI::Base)
|
139
|
+
raise(TypeError, "a JSI::Base instance must not be another JSI::Base. received: #{jsi_instance.pretty_inspect.chomp}")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# @!method jsi_schemas
|
144
|
+
# The set of schemas that describe this instance.
|
145
|
+
# These are the applicator schemas that apply to this instance, the result of inplace application
|
146
|
+
# of our {#jsi_indicated_schemas}.
|
147
|
+
# @return [JSI::SchemaSet]
|
148
|
+
# note: defined on subclasses by JSI::SchemaClasses.class_for_schemas
|
149
|
+
|
150
|
+
|
151
|
+
# document containing the instance of this JSI at our {#jsi_ptr}
|
152
|
+
attr_reader :jsi_document
|
153
|
+
|
154
|
+
# {JSI::Ptr} pointing to this JSI's instance within our {#jsi_document}
|
155
|
+
# @return [JSI::Ptr]
|
156
|
+
attr_reader :jsi_ptr
|
157
|
+
|
158
|
+
# the JSI at the root of this JSI's document
|
159
|
+
# @return [JSI::Base]
|
160
|
+
attr_reader :jsi_root_node
|
161
|
+
|
162
|
+
# the content of this node in our {#jsi_document} at our {#jsi_ptr}. the same as {#jsi_instance}.
|
163
|
+
def jsi_node_content
|
164
|
+
content = jsi_ptr.evaluate(jsi_document)
|
165
|
+
content
|
166
|
+
end
|
167
|
+
|
168
|
+
# the JSON schema instance this JSI represents - the underlying JSON data used to instantiate this JSI
|
169
|
+
alias_method :jsi_instance, :jsi_node_content
|
170
|
+
|
171
|
+
# the schemas indicated as describing this instance, prior to inplace application.
|
172
|
+
#
|
173
|
+
# this is different from {#jsi_schemas}, which are the inplace applicator schemas
|
174
|
+
# which describe this instance. for most purposes, `#jsi_schemas` is more relevant.
|
175
|
+
#
|
176
|
+
# `jsi_indicated_schemas` does not include inplace applicator schemas, such as the
|
177
|
+
# subschemas of `allOf`, whereas `#jsi_schemas` does.
|
178
|
+
#
|
179
|
+
# this does include indicated schemas which do not apply themselves, such as `$ref`
|
180
|
+
# schemas (on json schema drafts up to 7) - these are not included on `#jsi_schemas`.
|
181
|
+
#
|
182
|
+
# @return [JSI::SchemaSet]
|
183
|
+
attr_reader :jsi_indicated_schemas
|
184
|
+
|
185
|
+
# yields a JSI of each node at or below this one in this JSI's document.
|
186
|
+
#
|
187
|
+
# @param propertyNames [Boolean] Whether to also yield each object property
|
188
|
+
# name (Hash key) of any descendent which is a hash/object.
|
189
|
+
# These are described by `propertyNames` subshemas of that object's schemas.
|
190
|
+
# They are not actual descendents of this node.
|
191
|
+
# See {HashNode#jsi_each_propertyName}.
|
192
|
+
# @yield [JSI::Base] each descendent node, starting with self
|
193
|
+
# @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
|
194
|
+
def jsi_each_descendent_node(propertyNames: false, &block)
|
195
|
+
return to_enum(__method__, propertyNames: propertyNames) unless block
|
196
|
+
|
197
|
+
yield self
|
198
|
+
|
199
|
+
if propertyNames && is_a?(HashNode)
|
200
|
+
jsi_each_propertyName do |propertyName|
|
201
|
+
propertyName.jsi_each_descendent_node(propertyNames: propertyNames, &block)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
jsi_each_child_token do |token|
|
206
|
+
jsi_child(token, as_jsi: true).jsi_each_descendent_node(propertyNames: propertyNames, &block)
|
207
|
+
end
|
208
|
+
|
209
|
+
nil
|
210
|
+
end
|
211
|
+
|
212
|
+
# recursively selects descendent nodes of this JSI, returning a modified copy of self containing only
|
213
|
+
# descendent nodes for which the given block had a true-ish result.
|
214
|
+
#
|
215
|
+
# this method yields a node before recursively descending to its child nodes, so leaf nodes are yielded
|
216
|
+
# last, after their parents. if a node is not selected, its descendents are never recursed.
|
217
|
+
#
|
218
|
+
# @yield [JSI::Base] each descendent node below self
|
219
|
+
# @return [JSI::Base] modified copy of self containing only the selected nodes
|
220
|
+
def jsi_select_descendents_node_first(&block)
|
221
|
+
jsi_modified_copy do |instance|
|
222
|
+
if jsi_array? || jsi_hash?
|
223
|
+
res = instance.class.new
|
224
|
+
jsi_each_child_token do |token|
|
225
|
+
v = jsi_child(token, as_jsi: true)
|
226
|
+
if yield(v)
|
227
|
+
res_v = v.jsi_select_descendents_node_first(&block).jsi_node_content
|
228
|
+
if jsi_array?
|
229
|
+
res << res_v
|
230
|
+
else
|
231
|
+
res[token] = res_v
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
res
|
236
|
+
else
|
237
|
+
instance
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
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 jsi_array? || jsi_hash?
|
253
|
+
res = instance.class.new
|
254
|
+
jsi_each_child_token do |token|
|
255
|
+
v = jsi_child(token, as_jsi: true).jsi_select_descendents_leaf_first(&block)
|
256
|
+
if yield(v)
|
257
|
+
res_v = v.jsi_node_content
|
258
|
+
if jsi_array?
|
259
|
+
res << res_v
|
260
|
+
else
|
261
|
+
res[token] = res_v
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
res
|
266
|
+
else
|
267
|
+
instance
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# an array of JSI instances above this one in the document.
|
273
|
+
#
|
274
|
+
# @return [Array<JSI::Base>]
|
275
|
+
def jsi_parent_nodes
|
276
|
+
parent = jsi_root_node
|
277
|
+
|
278
|
+
jsi_ptr.tokens.map do |token|
|
279
|
+
parent.tap do
|
280
|
+
parent = parent[token, as_jsi: true]
|
281
|
+
end
|
282
|
+
end.reverse!.freeze
|
283
|
+
end
|
284
|
+
|
285
|
+
# the immediate parent of this JSI. nil if there is no parent.
|
286
|
+
#
|
287
|
+
# @return [JSI::Base, nil]
|
288
|
+
def jsi_parent_node
|
289
|
+
jsi_ptr.root? ? nil : jsi_root_node.jsi_descendent_node(jsi_ptr.parent)
|
290
|
+
end
|
291
|
+
|
292
|
+
# ancestor JSI instances from this node up to the root. this node itself is always its own first ancestor.
|
293
|
+
#
|
294
|
+
# @return [Array<JSI::Base>]
|
295
|
+
def jsi_ancestor_nodes
|
296
|
+
ancestors = []
|
297
|
+
ancestor = jsi_root_node
|
298
|
+
ancestors << ancestor
|
299
|
+
|
300
|
+
jsi_ptr.tokens.each do |token|
|
301
|
+
ancestor = ancestor[token, as_jsi: true]
|
302
|
+
ancestors << ancestor
|
303
|
+
end
|
304
|
+
ancestors.reverse!.freeze
|
305
|
+
end
|
306
|
+
|
307
|
+
# the descendent node at the given pointer
|
308
|
+
#
|
309
|
+
# @param ptr [JSI::Ptr, #to_ary]
|
310
|
+
# @return [JSI::Base]
|
311
|
+
def jsi_descendent_node(ptr)
|
312
|
+
descendent = Ptr.ary_ptr(ptr).evaluate(self, as_jsi: true)
|
313
|
+
descendent
|
314
|
+
end
|
315
|
+
|
316
|
+
# A shorthand alias for {#jsi_descendent_node}.
|
317
|
+
#
|
318
|
+
# Note that, though more convenient to type, using an operator whose meaning may not be intuitive
|
319
|
+
# to a reader could impair readability of code.
|
320
|
+
#
|
321
|
+
# examples:
|
322
|
+
#
|
323
|
+
# my_jsi / ['foo', 'bar']
|
324
|
+
# my_jsi / %w(foo bar)
|
325
|
+
# my_schema / JSI::Ptr['additionalProperties']
|
326
|
+
# my_schema / %w(properties foo items additionalProperties)
|
327
|
+
#
|
328
|
+
# @param (see #jsi_descendent_node)
|
329
|
+
# @return (see #jsi_descendent_node)
|
330
|
+
def /(ptr)
|
331
|
+
jsi_descendent_node(ptr)
|
332
|
+
end
|
333
|
+
|
334
|
+
# yields each token (array index or hash key) identifying a child node.
|
335
|
+
# yields nothing if this node is not complex or has no children.
|
336
|
+
#
|
337
|
+
# @yield [String, Integer] each child token
|
338
|
+
# @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
|
339
|
+
def jsi_each_child_token
|
340
|
+
# note: overridden by Base::HashNode, Base::ArrayNode
|
341
|
+
return to_enum(__method__) { 0 } unless block_given?
|
342
|
+
nil
|
343
|
+
end
|
344
|
+
|
345
|
+
# Does the given token identify a child of this node?
|
346
|
+
#
|
347
|
+
# In other words, is the given token an array index or hash key of the instance?
|
348
|
+
#
|
349
|
+
# Always false if this is not a complex node.
|
350
|
+
#
|
351
|
+
# @param token [String, Integer]
|
352
|
+
# @return [Boolean]
|
353
|
+
def jsi_child_token_in_range?(token)
|
354
|
+
# note: overridden by Base::HashNode, Base::ArrayNode
|
355
|
+
false
|
356
|
+
end
|
357
|
+
|
358
|
+
# The child of the {#jsi_node_content} identified by the given token,
|
359
|
+
# or `nil` if the token does not identify an existing child.
|
360
|
+
#
|
361
|
+
# In other words, the element of the instance array at the given index,
|
362
|
+
# or the value of the instance hash/object for the given key.
|
363
|
+
#
|
364
|
+
# @return [Object, nil]
|
365
|
+
# @raise [SimpleNodeChildError] if this node is not complex (its instance is not array or hash)
|
366
|
+
def jsi_node_content_child(token)
|
367
|
+
# note: overridden by Base::HashNode, Base::ArrayNode
|
368
|
+
jsi_simple_node_child_error(token)
|
369
|
+
end
|
370
|
+
|
371
|
+
# A child JSI node, or the child of our {#jsi_instance}, identified by the given token.
|
372
|
+
# The token must identify an existing child; behavior if the child does not exist is undefined.
|
373
|
+
#
|
374
|
+
# @param token (see Base#[])
|
375
|
+
# @param as_jsi (see Base#[])
|
376
|
+
# @return [JSI::Base, Object]
|
377
|
+
def jsi_child(token, as_jsi: )
|
378
|
+
child_content = jsi_node_content_child(token)
|
379
|
+
|
380
|
+
child_indicated_schemas = @child_indicated_schemas_map[token: token, content: jsi_node_content]
|
381
|
+
child_applied_schemas = @child_applied_schemas_map[token: token, child_indicated_schemas: child_indicated_schemas, child_content: child_content]
|
382
|
+
|
383
|
+
jsi_child_as_jsi(child_content, child_applied_schemas, as_jsi) do
|
384
|
+
@child_node_map[
|
385
|
+
token: token,
|
386
|
+
child_indicated_schemas: child_indicated_schemas,
|
387
|
+
child_applied_schemas: child_applied_schemas,
|
388
|
+
includes: SchemaClasses.includes_for(child_content),
|
389
|
+
]
|
390
|
+
end
|
391
|
+
end
|
392
|
+
private :jsi_child # internals for #[] but idk, could be public
|
393
|
+
|
394
|
+
# A default value for a child of this node identified by the given token, if schemas describing
|
395
|
+
# that child define a default value.
|
396
|
+
#
|
397
|
+
# If no schema describes a default value for the child (or in the unusual case that multiple
|
398
|
+
# schemas define different defaults), the result is `nil`.
|
399
|
+
#
|
400
|
+
# See also the `use_default` param of {Base#[]}.
|
401
|
+
#
|
402
|
+
# @param token (see Base#[])
|
403
|
+
# @param as_jsi (see Base#[])
|
404
|
+
# @return [JSI::Base, nil]
|
405
|
+
def jsi_default_child(token, as_jsi: )
|
406
|
+
child_content = jsi_node_content_child(token)
|
407
|
+
|
408
|
+
child_indicated_schemas = @child_indicated_schemas_map[token: token, content: jsi_node_content]
|
409
|
+
child_applied_schemas = @child_applied_schemas_map[token: token, child_indicated_schemas: child_indicated_schemas, child_content: child_content]
|
410
|
+
|
411
|
+
defaults = Set.new
|
412
|
+
child_applied_schemas.each do |child_schema|
|
413
|
+
if child_schema.keyword?('default')
|
414
|
+
defaults << child_schema.jsi_node_content['default']
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
if defaults.size == 1
|
419
|
+
# use the default value
|
420
|
+
jsi_child_as_jsi(defaults.first, child_applied_schemas, as_jsi) do
|
421
|
+
jsi_modified_copy do |i|
|
422
|
+
i.dup.tap { |i_dup| i_dup[token] = defaults.first }
|
423
|
+
end[token, as_jsi: true]
|
424
|
+
end
|
425
|
+
else
|
426
|
+
child_content
|
427
|
+
end
|
428
|
+
end
|
429
|
+
private :jsi_default_child # internals for #[] but idk, could be public
|
430
|
+
|
431
|
+
# subscripts to return a child value identified by the given token.
|
432
|
+
#
|
433
|
+
# @param token [String, Integer, Object] an array index or hash key (JSON object property name)
|
434
|
+
# of the instance identifying the child value
|
435
|
+
# @param as_jsi [:auto, true, false] (default is `:auto`)
|
436
|
+
# Whether to return the child as a JSI. One of:
|
437
|
+
#
|
438
|
+
# - `:auto`: By default a JSI will be returned when either:
|
439
|
+
#
|
440
|
+
# - the result is a complex value (responds to #to_ary or #to_hash)
|
441
|
+
# - the result is a schema (including true/false schemas)
|
442
|
+
#
|
443
|
+
# The plain content is returned when it is a simple type.
|
444
|
+
#
|
445
|
+
# - true: the result value will always be returned as a JSI. the {#jsi_schemas} of the result may be
|
446
|
+
# empty if no schemas describe the instance.
|
447
|
+
# - false: the result value will always be the plain instance.
|
448
|
+
#
|
449
|
+
# note that nil is returned (regardless of as_jsi) when there is no value to return because the token
|
450
|
+
# is not a hash key or array index of the instance and no default value applies.
|
451
|
+
# (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
|
452
|
+
# unspecified behavior.)
|
453
|
+
# @param use_default [true, false] (default is `false`)
|
454
|
+
# Whether to return a schema default value when the token refers to a child that is not in the document.
|
455
|
+
# If the token is not an array index or hash key of the instance, and one schema for the child
|
456
|
+
# instance specifies a default value, that default is returned.
|
457
|
+
#
|
458
|
+
# if the result with the default value is a JSI (per the `as_jsi` param), that JSI is not a child of
|
459
|
+
# this JSI - this JSI is not modified to fill in the default value. the result is a JSI within a new
|
460
|
+
# document containing the filled-in default.
|
461
|
+
#
|
462
|
+
# if the child instance's schemas do not indicate a single default value (that is, if zero or multiple
|
463
|
+
# defaults are specified across those schemas), nil is returned.
|
464
|
+
# (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
|
465
|
+
# unspecified behavior.)
|
466
|
+
# @return [JSI::Base, Object, nil] the child value identified by the subscript token
|
467
|
+
def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default)
|
468
|
+
# note: overridden by Base::HashNode, Base::ArrayNode
|
469
|
+
jsi_simple_node_child_error(token)
|
470
|
+
end
|
471
|
+
|
472
|
+
# The default value for the param `as_jsi` of {#[]}, controlling whether a child is returned as a JSI instance.
|
473
|
+
# @return [:auto, true, false] a valid value of the `as_jsi` param of {#[]}
|
474
|
+
def jsi_child_as_jsi_default
|
475
|
+
:auto
|
476
|
+
end
|
477
|
+
|
478
|
+
# The default value for the param `use_default` of {#[]}, controlling whether a schema default value is
|
479
|
+
# returned when a token refers to a child that is not in the document.
|
480
|
+
# @return [true, false] a valid value of the `use_default` param of {#[]}
|
481
|
+
def jsi_child_use_default_default
|
482
|
+
false
|
483
|
+
end
|
484
|
+
|
485
|
+
# assigns the subscript of the instance identified by the given token to the given value.
|
486
|
+
# if the value is a JSI, its instance is assigned instead of the JSI value itself.
|
487
|
+
#
|
488
|
+
# @param token [String, Integer, Object] token identifying the subscript to assign
|
489
|
+
# @param value [JSI::Base, Object] the value to be assigned
|
490
|
+
def []=(token, value)
|
491
|
+
unless jsi_array? || jsi_hash?
|
492
|
+
jsi_simple_node_child_error(token)
|
493
|
+
end
|
494
|
+
if value.is_a?(Base)
|
495
|
+
self[token] = value.jsi_instance
|
496
|
+
else
|
497
|
+
jsi_instance[token] = value
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
# the set of JSI schema modules corresponding to the schemas that describe this JSI
|
502
|
+
# @return [Set<Module>]
|
503
|
+
def jsi_schema_modules
|
504
|
+
Util.ensure_module_set(jsi_schemas.map(&:jsi_schema_module))
|
505
|
+
end
|
506
|
+
|
507
|
+
# Is this JSI described by the given schema (or schema module)?
|
508
|
+
#
|
509
|
+
# @param schema [Schema, SchemaModule]
|
510
|
+
# @return [Boolean]
|
511
|
+
def described_by?(schema)
|
512
|
+
if schema.is_a?(Schema)
|
513
|
+
jsi_schemas.include?(schema)
|
514
|
+
elsif schema.is_a?(SchemaModule)
|
515
|
+
jsi_schema_modules.include?(schema)
|
516
|
+
else
|
517
|
+
raise(TypeError, "expected a Schema or Schema Module; got: #{schema.pretty_inspect.chomp}")
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
# yields the content of this JSI's instance. the block must result in
|
522
|
+
# a modified copy of the yielded instance (not modified in place, which would alter this JSI
|
523
|
+
# as well) which will be used to instantiate and return a new JSI with the modified content.
|
524
|
+
#
|
525
|
+
# the result may have different schemas which describe it than this JSI's schemas,
|
526
|
+
# if conditional applicator schemas apply differently to the modified instance.
|
527
|
+
#
|
528
|
+
# @yield [Object] this JSI's instance. the block should result
|
529
|
+
# in a nondestructively modified copy of this.
|
530
|
+
# @return [JSI::Base subclass] the modified copy of self
|
531
|
+
def jsi_modified_copy(&block)
|
532
|
+
modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
|
533
|
+
modified_jsi_root_node = @jsi_root_node.jsi_indicated_schemas.new_jsi(modified_document,
|
534
|
+
uri: @jsi_root_node.jsi_schema_base_uri,
|
535
|
+
register: false, # default is already false but this is a place to be explicit
|
536
|
+
schema_registry: jsi_schema_registry,
|
537
|
+
)
|
538
|
+
modified_jsi_root_node.jsi_descendent_node(@jsi_ptr)
|
539
|
+
end
|
540
|
+
|
541
|
+
# Is the instance an array?
|
542
|
+
#
|
543
|
+
# An array is typically an instance of the Array class but may be an object that supports
|
544
|
+
# [implicit conversion](https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html)
|
545
|
+
# with a `#to_ary` method.
|
546
|
+
#
|
547
|
+
# @return [Boolean]
|
548
|
+
def jsi_array?
|
549
|
+
# note: overridden by Base::ArrayNode
|
550
|
+
false
|
551
|
+
end
|
552
|
+
|
553
|
+
# Is the instance a ruby Hash (JSON object)?
|
554
|
+
#
|
555
|
+
# This is typically an instance of the Hash class but may be an object that supports
|
556
|
+
# [implicit conversion](https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html)
|
557
|
+
# with a `#to_hash` method.
|
558
|
+
#
|
559
|
+
# @return [Boolean]
|
560
|
+
def jsi_hash?
|
561
|
+
# note: overridden by Base::HashNode
|
562
|
+
false
|
563
|
+
end
|
564
|
+
|
565
|
+
# validates this JSI's instance against its schemas
|
566
|
+
#
|
567
|
+
# @return [JSI::Validation::FullResult]
|
568
|
+
def jsi_validate
|
569
|
+
jsi_indicated_schemas.instance_validate(self)
|
570
|
+
end
|
571
|
+
|
572
|
+
# whether this JSI's instance is valid against all of its schemas
|
573
|
+
# @return [Boolean]
|
574
|
+
def jsi_valid?
|
575
|
+
jsi_indicated_schemas.instance_valid?(self)
|
576
|
+
end
|
577
|
+
|
578
|
+
# queries this JSI using the [JMESPath Ruby](https://rubygems.org/gems/jmespath) gem.
|
579
|
+
# see [https://jmespath.org/](https://jmespath.org/) to learn the JMESPath query language.
|
580
|
+
#
|
581
|
+
# the JMESPath gem is not a dependency of JSI, so must be installed / added to your Gemfile to use.
|
582
|
+
# e.g. `gem 'jmespath', '~> 1.5'`. note that versions below 1.5 are not compatible with JSI.
|
583
|
+
#
|
584
|
+
# @param expression [String] a [JMESPath](https://jmespath.org/) expression
|
585
|
+
# @param runtime_options passed to [JMESPath.search](https://rubydoc.info/gems/jmespath/JMESPath#search-class_method),
|
586
|
+
# though no runtime_options are publicly documented or normally used.
|
587
|
+
# @return [Array, Object, nil] query results.
|
588
|
+
# see [JMESPath.search](https://rubydoc.info/gems/jmespath/JMESPath#search-class_method)
|
589
|
+
def jmespath_search(expression, **runtime_options)
|
590
|
+
Util.require_jmespath
|
591
|
+
|
592
|
+
JMESPath.search(expression, self, **runtime_options)
|
593
|
+
end
|
594
|
+
|
595
|
+
def dup
|
596
|
+
jsi_modified_copy(&:dup)
|
597
|
+
end
|
598
|
+
|
599
|
+
# a string representing this JSI, indicating any named schemas and inspecting its instance
|
600
|
+
# @return [String]
|
601
|
+
def inspect
|
602
|
+
-"\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
|
603
|
+
end
|
604
|
+
|
605
|
+
alias_method :to_s, :inspect
|
606
|
+
|
607
|
+
# pretty-prints a representation of this JSI to the given printer
|
608
|
+
# @return [void]
|
609
|
+
def pretty_print(q)
|
610
|
+
q.text '#<'
|
611
|
+
q.text jsi_object_group_text.join(' ')
|
612
|
+
q.group_sub {
|
613
|
+
q.nest(2) {
|
614
|
+
q.breakable ' '
|
615
|
+
q.pp jsi_instance
|
616
|
+
}
|
617
|
+
}
|
618
|
+
q.breakable ''
|
619
|
+
q.text '>'
|
620
|
+
end
|
621
|
+
|
622
|
+
# @private
|
623
|
+
# @return [Array<String>]
|
624
|
+
def jsi_object_group_text
|
625
|
+
schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name_from_ancestor || schema.schema_uri }.compact
|
626
|
+
if schema_names.empty?
|
627
|
+
class_txt = "JSI"
|
628
|
+
else
|
629
|
+
class_txt = -"JSI (#{schema_names.join(', ')})"
|
630
|
+
end
|
631
|
+
|
632
|
+
if (is_a?(ArrayNode) || is_a?(HashNode)) && ![Array, Hash].include?(jsi_node_content.class)
|
633
|
+
if jsi_node_content.respond_to?(:jsi_object_group_text)
|
634
|
+
content_txt = jsi_node_content.jsi_object_group_text
|
635
|
+
else
|
636
|
+
content_txt = jsi_node_content.class.to_s
|
637
|
+
end
|
638
|
+
else
|
639
|
+
content_txt = nil
|
640
|
+
end
|
641
|
+
|
642
|
+
[
|
643
|
+
class_txt,
|
644
|
+
is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
|
645
|
+
*content_txt,
|
646
|
+
].compact
|
647
|
+
end
|
648
|
+
|
649
|
+
# A structure coerced to JSONifiable types from the instance content.
|
650
|
+
# Calls {Util.as_json} with the instance and any given options.
|
651
|
+
def as_json(options = {})
|
652
|
+
Util.as_json(jsi_instance, **options)
|
653
|
+
end
|
654
|
+
|
655
|
+
# A JSON encoded string of the instance content.
|
656
|
+
# Calls {Util.to_json} with the instance and any given options.
|
657
|
+
# @return [String]
|
658
|
+
def to_json(options = {})
|
659
|
+
Util.to_json(jsi_instance, **options)
|
660
|
+
end
|
661
|
+
|
662
|
+
# see {Util::Private::FingerprintHash}
|
663
|
+
# @api private
|
664
|
+
def jsi_fingerprint
|
665
|
+
{
|
666
|
+
class: jsi_class,
|
667
|
+
jsi_document: jsi_document,
|
668
|
+
jsi_ptr: jsi_ptr,
|
669
|
+
# for instances in documents with schemas:
|
670
|
+
jsi_resource_ancestor_uri: jsi_resource_ancestor_uri,
|
671
|
+
# different registries mean references may resolve to different resources so must not be equal
|
672
|
+
jsi_schema_registry: jsi_schema_registry,
|
673
|
+
}
|
674
|
+
end
|
675
|
+
include Util::FingerprintHash
|
676
|
+
|
677
|
+
private
|
678
|
+
|
679
|
+
def jsi_memomaps_initialize
|
680
|
+
@child_indicated_schemas_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_indicated_schemas_compute))
|
681
|
+
@child_applied_schemas_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_applied_schemas_compute))
|
682
|
+
@child_node_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_node_compute))
|
683
|
+
end
|
684
|
+
|
685
|
+
def jsi_indicated_schemas=(jsi_indicated_schemas)
|
686
|
+
#chkbug raise(Bug) unless jsi_indicated_schemas.is_a?(SchemaSet)
|
687
|
+
@jsi_indicated_schemas = jsi_indicated_schemas
|
688
|
+
end
|
689
|
+
|
690
|
+
def jsi_child_node_compute(token: , child_indicated_schemas: , child_applied_schemas: , includes: )
|
691
|
+
jsi_class = JSI::SchemaClasses.class_for_schemas(child_applied_schemas,
|
692
|
+
includes: includes,
|
693
|
+
)
|
694
|
+
jsi_class.new(@jsi_document,
|
695
|
+
jsi_ptr: @jsi_ptr[token],
|
696
|
+
jsi_indicated_schemas: child_indicated_schemas,
|
697
|
+
jsi_schema_base_uri: jsi_resource_ancestor_uri,
|
698
|
+
jsi_schema_resource_ancestors: is_a?(Schema) ? jsi_subschema_resource_ancestors : jsi_schema_resource_ancestors,
|
699
|
+
jsi_schema_registry: jsi_schema_registry,
|
700
|
+
jsi_root_node: @jsi_root_node,
|
701
|
+
)
|
702
|
+
end
|
703
|
+
|
704
|
+
def jsi_child_indicated_schemas_compute(token: , content: )
|
705
|
+
jsi_schemas.child_applicator_schemas(token, content)
|
706
|
+
end
|
707
|
+
|
708
|
+
def jsi_child_applied_schemas_compute(token: , child_indicated_schemas: , child_content: )
|
709
|
+
child_indicated_schemas.inplace_applicator_schemas(child_content)
|
710
|
+
end
|
711
|
+
|
712
|
+
def jsi_child_as_jsi(child_content, child_schemas, as_jsi)
|
713
|
+
if [true, false].include?(as_jsi)
|
714
|
+
child_as_jsi = as_jsi
|
715
|
+
elsif as_jsi == :auto
|
716
|
+
child_is_complex = child_content.respond_to?(:to_hash) || child_content.respond_to?(:to_ary)
|
717
|
+
child_is_schema = child_schemas.any?(&:describes_schema?)
|
718
|
+
child_as_jsi = child_is_complex || child_is_schema
|
719
|
+
else
|
720
|
+
raise(ArgumentError, "as_jsi must be one of: :auto, true, false")
|
721
|
+
end
|
722
|
+
|
723
|
+
if child_as_jsi
|
724
|
+
yield
|
725
|
+
else
|
726
|
+
child_content
|
727
|
+
end
|
728
|
+
end
|
729
|
+
|
730
|
+
def jsi_simple_node_child_error(token)
|
731
|
+
raise(SimpleNodeChildError, [
|
732
|
+
"cannot access a child of this JSI node because this node is not complex",
|
733
|
+
"using token: #{token.inspect}",
|
734
|
+
"instance: #{jsi_instance.pretty_inspect.chomp}",
|
735
|
+
].join("\n"))
|
736
|
+
end
|
737
|
+
end
|
738
|
+
end
|