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