jsi 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|