jsi 0.0.1 → 0.0.2
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 -0
- data/CHANGELOG.md +7 -0
- data/README.md +49 -17
- data/lib/jsi.rb +4 -1
- data/lib/jsi/base.rb +147 -26
- data/lib/jsi/base/to_rb.rb +2 -3
- data/lib/jsi/json-schema-fragments.rb +6 -5
- data/lib/jsi/json/node.rb +117 -46
- data/lib/jsi/schema.rb +94 -62
- data/lib/jsi/typelike_modules.rb +65 -15
- data/lib/jsi/util.rb +30 -16
- data/lib/jsi/version.rb +1 -1
- data/test/base_array_test.rb +2 -2
- data/test/base_hash_test.rb +7 -7
- data/test/base_test.rb +19 -19
- data/test/jsi_json_arraynode_test.rb +133 -117
- data/test/jsi_json_hashnode_test.rb +116 -102
- data/test/jsi_json_node_test.rb +13 -13
- data/test/schema_instance_json_coder_test.rb +6 -7
- data/test/schema_test.rb +196 -0
- data/test/struct_json_coder_test.rb +2 -2
- data/test/test_helper.rb +36 -2
- data/test/util_test.rb +4 -4
- metadata +5 -2
data/lib/jsi/base/to_rb.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
module JSI
|
2
|
-
# base class for representing an instance of an instance described by a schema
|
3
2
|
class Base
|
4
3
|
class << self
|
5
4
|
def class_comment
|
@@ -15,7 +14,7 @@ module JSI
|
|
15
14
|
lines << "#"
|
16
15
|
end
|
17
16
|
|
18
|
-
schema.
|
17
|
+
schema.described_object_property_names.each_with_index do |propname, i|
|
19
18
|
lines << "#" unless i == 0
|
20
19
|
lines << "# @!attribute [rw] #{propname}"
|
21
20
|
|
@@ -73,7 +72,7 @@ module JSI
|
|
73
72
|
end
|
74
73
|
end
|
75
74
|
lines << "class #{name}"
|
76
|
-
schema.
|
75
|
+
schema.described_object_property_names.each_with_index do |propname, i|
|
77
76
|
lines << "" unless i == 0
|
78
77
|
property_schema = schema['properties'].respond_to?(:to_hash) &&
|
79
78
|
schema['properties'][propname].respond_to?(:to_hash) &&
|
@@ -84,24 +84,25 @@ module JSON
|
|
84
84
|
# pointed to by this pointer.
|
85
85
|
def evaluate(document)
|
86
86
|
reference_tokens.inject(document) do |value, token|
|
87
|
-
if value.
|
87
|
+
if value.respond_to?(:to_ary)
|
88
88
|
if token.is_a?(String) && token =~ /\A\d|[1-9]\d+\z/
|
89
89
|
token = token.to_i
|
90
90
|
end
|
91
91
|
unless token.is_a?(Integer)
|
92
92
|
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not an integer and cannot be resolved in array #{value.inspect}")
|
93
93
|
end
|
94
|
-
unless (0...value.size).include?(token)
|
94
|
+
unless (0...(value.respond_to?(:size) ? value : value.to_ary).size).include?(token)
|
95
95
|
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid index of #{value.inspect}")
|
96
96
|
end
|
97
|
-
|
98
|
-
|
97
|
+
(value.respond_to?(:[]) ? value : value.to_ary)[token]
|
98
|
+
elsif value.respond_to?(:to_hash)
|
99
|
+
unless (value.respond_to?(:key?) ? value : value.to_hash).key?(token)
|
99
100
|
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid key of #{value.inspect}")
|
100
101
|
end
|
102
|
+
(value.respond_to?(:[]) ? value : value.to_hash)[token]
|
101
103
|
else
|
102
104
|
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} cannot be resolved in #{value.inspect}")
|
103
105
|
end
|
104
|
-
value[token]
|
105
106
|
end
|
106
107
|
end
|
107
108
|
|
data/lib/jsi/json/node.rb
CHANGED
@@ -2,25 +2,40 @@ module JSI
|
|
2
2
|
module JSON
|
3
3
|
# JSI::JSON::Node is an abstraction of a node within a JSON document.
|
4
4
|
# it aims to act like the underlying data type of the node's content
|
5
|
-
# (Hash or Array
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
5
|
+
# (generally Hash or Array-like) in most cases.
|
6
|
+
#
|
7
|
+
# the main advantage offered by using a Node over the underlying data
|
8
|
+
# is in dereferencing. if a Node consists of a hash with a $ref property
|
9
|
+
# pointing within the same document, then the Node will transparently
|
10
|
+
# follow the ref and return the referenced data.
|
11
|
+
#
|
12
|
+
# in most other respects, a Node aims to act like a Hash when the content
|
13
|
+
# is Hash-like, an Array when the content is Array-like. methods of Hash
|
14
|
+
# and Array are defined and delegated to the node's content.
|
15
|
+
#
|
16
|
+
# however, destructive methods are for the most part not implemented.
|
17
|
+
# at the moment only #[]= is implemented. since Node thinly wraps the
|
18
|
+
# underlying data, you can change the data and it will be reflected in
|
19
|
+
# the node. implementations of destructive methods are planned.
|
9
20
|
#
|
10
21
|
# methods that return a modified copy such as #merge are defined, and
|
11
22
|
# return a copy of the document with the content of the node modified.
|
12
23
|
# the original node's document and content are untouched.
|
13
24
|
class Node
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
25
|
+
def self.new_doc(document)
|
26
|
+
new_by_type(document, [])
|
27
|
+
end
|
28
|
+
|
29
|
+
# if the content of the document at the given path is Hash-like, returns
|
30
|
+
# a HashNode; if Array-like, returns ArrayNode. otherwise returns a
|
31
|
+
# regular Node, although Nodes are for the most part instantiated from
|
32
|
+
# Hash or Array-like content.
|
18
33
|
def self.new_by_type(document, path)
|
19
34
|
node = Node.new(document, path)
|
20
35
|
content = node.content
|
21
|
-
if content.
|
36
|
+
if content.respond_to?(:to_hash)
|
22
37
|
HashNode.new(document, path)
|
23
|
-
elsif content.
|
38
|
+
elsif content.respond_to?(:to_ary)
|
24
39
|
ArrayNode.new(document, path)
|
25
40
|
else
|
26
41
|
node
|
@@ -29,9 +44,14 @@ module JSI
|
|
29
44
|
|
30
45
|
# a Node represents the content of a document at a given path.
|
31
46
|
def initialize(document, path)
|
32
|
-
|
47
|
+
unless path.respond_to?(:to_ary)
|
48
|
+
raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect.chomp} (#{path.class})")
|
49
|
+
end
|
50
|
+
if document.is_a?(JSI::JSON::Node)
|
51
|
+
raise(TypeError, "document of a Node should not be another JSI::JSON::Node: #{document.inspect}")
|
52
|
+
end
|
33
53
|
@document = document
|
34
|
-
@path = path.dup.freeze
|
54
|
+
@path = path.to_ary.dup.freeze
|
35
55
|
@pointer = ::JSON::Schema::Pointer.new(:reference_tokens, path)
|
36
56
|
end
|
37
57
|
|
@@ -47,60 +67,88 @@ module JSI
|
|
47
67
|
pointer.evaluate(document)
|
48
68
|
end
|
49
69
|
|
50
|
-
|
70
|
+
# returns content at the given subscript - call this the subcontent.
|
71
|
+
#
|
72
|
+
# if the content cannot be subscripted, raises TypeError.
|
73
|
+
#
|
74
|
+
# if the subcontent is Hash-like, it is wrapped as a JSI::JSON::HashNode before being returned.
|
75
|
+
# if the subcontent is Array-like, it is wrapped as a JSI::JSON::ArrayNode before being returned.
|
76
|
+
#
|
77
|
+
# if this node's content is a $ref - that is, a hash with a $ref attribute - and the subscript is
|
78
|
+
# not a key of the hash, then the $ref is followed before returning the subcontent.
|
79
|
+
def [](subscript)
|
51
80
|
node = self
|
52
81
|
content = node.content
|
53
|
-
if content.
|
82
|
+
if content.respond_to?(:to_hash) && !(content.respond_to?(:key?) ? content : content.to_hash).key?(subscript)
|
54
83
|
node = node.deref
|
55
84
|
content = node.content
|
56
85
|
end
|
86
|
+
unless content.respond_to?(:[])
|
87
|
+
if content.respond_to?(:to_hash)
|
88
|
+
content = content.to_hash
|
89
|
+
elsif content.respond_to?(:to_ary)
|
90
|
+
content = content.to_ary
|
91
|
+
else
|
92
|
+
raise(NoMethodError, "undefined method `[]`\nsubscripting with #{subscript.pretty_inspect.chomp} (#{subscript.class}) from #{content.class.inspect}. content is: #{content.pretty_inspect.chomp}")
|
93
|
+
end
|
94
|
+
end
|
57
95
|
begin
|
58
|
-
|
96
|
+
subcontent = content[subscript]
|
59
97
|
rescue TypeError => e
|
60
|
-
raise(e.class, e.message + "\nsubscripting with #{
|
98
|
+
raise(e.class, e.message + "\nsubscripting with #{subscript.pretty_inspect.chomp} (#{subscript.class}) from #{content.class.inspect}. content is: #{content.pretty_inspect.chomp}", e.backtrace)
|
61
99
|
end
|
62
|
-
if
|
63
|
-
|
100
|
+
if subcontent.respond_to?(:to_hash)
|
101
|
+
HashNode.new(node.document, node.path + [subscript])
|
102
|
+
elsif subcontent.respond_to?(:to_ary)
|
103
|
+
ArrayNode.new(node.document, node.path + [subscript])
|
64
104
|
else
|
65
|
-
|
105
|
+
subcontent
|
66
106
|
end
|
67
107
|
end
|
68
108
|
|
69
|
-
|
70
|
-
|
71
|
-
|
109
|
+
# assigns the given subscript of the content to the given value. the document is modified in place.
|
110
|
+
def []=(subscript, value)
|
111
|
+
if value.is_a?(Node)
|
112
|
+
content[subscript] = value.content
|
72
113
|
else
|
73
|
-
content[
|
114
|
+
content[subscript] = value
|
74
115
|
end
|
75
116
|
end
|
76
117
|
|
118
|
+
# returns a Node, dereferencing a $ref attribute if possible. if this node is not hash-like,
|
119
|
+
# does not have a $ref, or if what its $ref cannot be found, this node is returned.
|
120
|
+
#
|
121
|
+
# currently only $refs pointing within the same document are followed.
|
77
122
|
def deref
|
78
123
|
content = self.content
|
79
124
|
|
80
|
-
|
125
|
+
if content.respond_to?(:to_hash)
|
126
|
+
ref = (content.respond_to?(:[]) ? content : content.to_hash)['$ref']
|
127
|
+
end
|
128
|
+
return self unless ref.is_a?(String)
|
81
129
|
|
82
|
-
if
|
83
|
-
return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(
|
130
|
+
if ref[/\A#/]
|
131
|
+
return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(ref)).deref
|
84
132
|
end
|
85
133
|
|
86
134
|
# HAX for how google does refs and ids
|
87
135
|
if document_node['schemas'].respond_to?(:to_hash)
|
88
|
-
if document_node['schemas'][
|
89
|
-
return document_node['schemas'][
|
136
|
+
if document_node['schemas'][ref]
|
137
|
+
return document_node['schemas'][ref]
|
90
138
|
end
|
91
|
-
_, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] ==
|
139
|
+
_, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == ref }
|
92
140
|
if deref_by_id
|
93
141
|
return deref_by_id
|
94
142
|
end
|
95
143
|
end
|
96
144
|
|
97
|
-
#raise(NotImplementedError, "cannot dereference #{
|
145
|
+
#raise(NotImplementedError, "cannot dereference #{ref}") # TODO
|
98
146
|
return self
|
99
147
|
end
|
100
148
|
|
101
149
|
# a Node at the root of the document
|
102
150
|
def document_node
|
103
|
-
Node.
|
151
|
+
Node.new_doc(document)
|
104
152
|
end
|
105
153
|
|
106
154
|
# the parent of this node. if this node is the document root (its path is empty), raises
|
@@ -117,11 +165,13 @@ module JSI
|
|
117
165
|
def pointer_path
|
118
166
|
pointer.pointer
|
119
167
|
end
|
168
|
+
|
120
169
|
# the pointer fragment to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
|
121
170
|
def fragment
|
122
171
|
pointer.fragment
|
123
172
|
end
|
124
173
|
|
174
|
+
# returns a jsonifiable representation of this node's content
|
125
175
|
def as_json(*opt)
|
126
176
|
Typelike.as_json(content, *opt)
|
127
177
|
end
|
@@ -136,7 +186,7 @@ module JSI
|
|
136
186
|
# or hash in the path above this node. this node's content is modified by the caller, and
|
137
187
|
# that is recursively merged up to the document root. the recursion is done with a
|
138
188
|
# y combinator, for no other reason than that was a fun way to implement it.
|
139
|
-
modified_document = JSI.ycomb do |rec|
|
189
|
+
modified_document = JSI::Util.ycomb do |rec|
|
140
190
|
proc do |subdocument, subpath|
|
141
191
|
if subpath == []
|
142
192
|
yield(subdocument)
|
@@ -144,11 +194,12 @@ module JSI
|
|
144
194
|
car = subpath[0]
|
145
195
|
cdr = subpath[1..-1]
|
146
196
|
if subdocument.respond_to?(:to_hash)
|
147
|
-
|
148
|
-
|
197
|
+
subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_hash)[car]
|
198
|
+
car_object = rec.call(subdocument_car, cdr)
|
199
|
+
if car_object.object_id == subdocument_car.object_id
|
149
200
|
subdocument
|
150
201
|
else
|
151
|
-
subdocument.merge({car => car_object})
|
202
|
+
(subdocument.respond_to?(:merge) ? subdocument : subdocument.to_hash).merge({car => car_object})
|
152
203
|
end
|
153
204
|
elsif subdocument.respond_to?(:to_ary)
|
154
205
|
if car.is_a?(String) && car =~ /\A\d+\z/
|
@@ -157,11 +208,12 @@ module JSI
|
|
157
208
|
unless car.is_a?(Integer)
|
158
209
|
raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
|
159
210
|
end
|
160
|
-
|
161
|
-
|
211
|
+
subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_ary)[car]
|
212
|
+
car_object = rec.call(subdocument_car, cdr)
|
213
|
+
if car_object.object_id == subdocument_car.object_id
|
162
214
|
subdocument
|
163
215
|
else
|
164
|
-
subdocument.dup.tap do |arr|
|
216
|
+
(subdocument.respond_to?(:[]=) ? subdocument : subdocument.to_ary).dup.tap do |arr|
|
165
217
|
arr[car] = car_object
|
166
218
|
end
|
167
219
|
end
|
@@ -174,12 +226,17 @@ module JSI
|
|
174
226
|
Node.new_by_type(modified_document, path)
|
175
227
|
end
|
176
228
|
|
229
|
+
# meta-information about the object, outside the content. used by #inspect / #pretty_print
|
177
230
|
def object_group_text
|
178
|
-
"fragment=#{fragment.inspect}"
|
231
|
+
"fragment=#{fragment.inspect}" + (content.respond_to?(:object_group_text) ? ' ' + content.object_group_text : '')
|
179
232
|
end
|
233
|
+
|
234
|
+
# a string representing this node
|
180
235
|
def inspect
|
181
236
|
"\#<#{self.class.inspect} #{object_group_text} #{content.inspect}>"
|
182
237
|
end
|
238
|
+
|
239
|
+
# pretty-prints a representation this node to the given printer
|
183
240
|
def pretty_print(q)
|
184
241
|
q.instance_exec(self) do |obj|
|
185
242
|
text "\#<#{obj.class.inspect} #{obj.object_group_text}"
|
@@ -194,19 +251,27 @@ module JSI
|
|
194
251
|
end
|
195
252
|
end
|
196
253
|
|
254
|
+
# fingerprint for equality (see FingerprintHash). two nodes are equal if they are both nodes
|
255
|
+
# (regardless of type, e.g. one may be a Node and the other may be a HashNode) within equal
|
256
|
+
# documents at equal paths. note that this means two nodes with the same content may not be
|
257
|
+
# considered equal.
|
197
258
|
def fingerprint
|
198
259
|
{is_node: self.is_a?(JSI::JSON::Node), document: document, path: path}
|
199
260
|
end
|
200
261
|
include FingerprintHash
|
201
262
|
end
|
202
263
|
|
264
|
+
# a JSI::JSON::Node whose content is Array-like (responds to #to_ary)
|
265
|
+
# and includes Array methods from Arraylike
|
203
266
|
class ArrayNode < Node
|
267
|
+
# iterates over each element in the same manner as Array#each
|
204
268
|
def each
|
205
|
-
return to_enum(__method__) { content.size } unless block_given?
|
206
|
-
content.each_index { |i| yield self[i] }
|
269
|
+
return to_enum(__method__) { (content.respond_to?(:size) ? content : content.to_ary).size } unless block_given?
|
270
|
+
(content.respond_to?(:each_index) ? content : content.to_ary).each_index { |i| yield self[i] }
|
207
271
|
self
|
208
272
|
end
|
209
273
|
|
274
|
+
# the content of this ArrayNode, as an Array
|
210
275
|
def to_ary
|
211
276
|
to_a
|
212
277
|
end
|
@@ -214,6 +279,7 @@ module JSI
|
|
214
279
|
include Enumerable
|
215
280
|
include Arraylike
|
216
281
|
|
282
|
+
# returns a jsonifiable representation of this node's content
|
217
283
|
def as_json(*opt) # needs redefined after including Enumerable
|
218
284
|
Typelike.as_json(content, *opt)
|
219
285
|
end
|
@@ -221,21 +287,25 @@ module JSI
|
|
221
287
|
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
|
222
288
|
# we override these methods from Arraylike
|
223
289
|
SAFE_INDEX_ONLY_METHODS.each do |method_name|
|
224
|
-
define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
|
290
|
+
define_method(method_name) { |*a, &b| (content.respond_to?(method_name) ? content : content.to_ary).public_send(method_name, *a, &b) }
|
225
291
|
end
|
226
292
|
end
|
227
293
|
|
294
|
+
# a JSI::JSON::Node whose content is Hash-like (responds to #to_hash)
|
295
|
+
# and includes Hash methods from Hashlike
|
228
296
|
class HashNode < Node
|
297
|
+
# iterates over each element in the same manner as Array#each
|
229
298
|
def each(&block)
|
230
|
-
return to_enum(__method__) { content.size } unless block_given?
|
299
|
+
return to_enum(__method__) { content.respond_to?(:size) ? content.size : content.to_ary.size } unless block_given?
|
231
300
|
if block.arity > 1
|
232
|
-
content.each_key { |k| yield k, self[k] }
|
301
|
+
(content.respond_to?(:each_key) ? content : content.to_hash).each_key { |k| yield k, self[k] }
|
233
302
|
else
|
234
|
-
content.each_key { |k| yield [k, self[k]] }
|
303
|
+
(content.respond_to?(:each_key) ? content : content.to_hash).each_key { |k| yield [k, self[k]] }
|
235
304
|
end
|
236
305
|
self
|
237
306
|
end
|
238
307
|
|
308
|
+
# the content of this HashNode, as a Hash
|
239
309
|
def to_hash
|
240
310
|
inject({}) { |h, (k, v)| h[k] = v; h }
|
241
311
|
end
|
@@ -243,13 +313,14 @@ module JSI
|
|
243
313
|
include Enumerable
|
244
314
|
include Hashlike
|
245
315
|
|
316
|
+
# returns a jsonifiable representation of this node's content
|
246
317
|
def as_json(*opt) # needs redefined after including Enumerable
|
247
318
|
Typelike.as_json(content, *opt)
|
248
319
|
end
|
249
320
|
|
250
321
|
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
|
251
322
|
SAFE_KEY_ONLY_METHODS.each do |method_name|
|
252
|
-
define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
|
323
|
+
define_method(method_name) { |*a, &b| (content.respond_to?(method_name) ? content : content.to_hash).public_send(method_name, *a, &b) }
|
253
324
|
end
|
254
325
|
end
|
255
326
|
end
|
data/lib/jsi/schema.rb
CHANGED
@@ -1,37 +1,50 @@
|
|
1
1
|
require 'jsi/json/node'
|
2
2
|
|
3
3
|
module JSI
|
4
|
+
# JSI::Schema represents a JSON Schema. initialized from a Hash-like schema
|
5
|
+
# object, JSI::Schema is a relatively simple class to abstract useful methods
|
6
|
+
# applied to a JSON Schema.
|
4
7
|
class Schema
|
5
8
|
include Memoize
|
6
9
|
|
10
|
+
# initializes a schema from a given JSI::Base, JSI::JSON::Node, or hash.
|
11
|
+
# @param schema_object [JSI::Base, #to_hash] the schema
|
7
12
|
def initialize(schema_object)
|
8
13
|
if schema_object.is_a?(JSI::Schema)
|
9
14
|
raise(TypeError, "will not instantiate Schema from another Schema: #{schema_object.pretty_inspect.chomp}")
|
10
15
|
elsif schema_object.is_a?(JSI::Base)
|
11
|
-
@
|
12
|
-
@schema_node = @
|
16
|
+
@schema_jsi = JSI.deep_stringify_symbol_keys(schema_object.deref)
|
17
|
+
@schema_node = @schema_jsi.instance
|
13
18
|
elsif schema_object.is_a?(JSI::JSON::HashNode)
|
14
|
-
@
|
19
|
+
@schema_jsi = nil
|
15
20
|
@schema_node = JSI.deep_stringify_symbol_keys(schema_object.deref)
|
16
21
|
elsif schema_object.respond_to?(:to_hash)
|
17
|
-
@
|
18
|
-
@schema_node = JSI::JSON::Node.
|
22
|
+
@schema_jsi = nil
|
23
|
+
@schema_node = JSI::JSON::Node.new_doc(JSI.deep_stringify_symbol_keys(schema_object))
|
19
24
|
else
|
20
25
|
raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
|
21
26
|
end
|
22
|
-
if @schema_object
|
23
|
-
define_singleton_method(:instance) { schema_node } # aka schema_object.instance
|
24
|
-
define_singleton_method(:schema) { schema_object.schema }
|
25
|
-
extend BaseHash
|
26
|
-
else
|
27
|
-
define_singleton_method(:[]) { |*a, &b| schema_node.public_send(:[], *a, &b) }
|
28
|
-
end
|
29
27
|
end
|
28
|
+
|
29
|
+
# @return [JSI::JSON::Node] a JSI::JSON::Node for the schema
|
30
30
|
attr_reader :schema_node
|
31
|
+
|
32
|
+
# @return [JSI::Base, nil] a JSI for this schema, if a metaschema is known; otherwise nil
|
33
|
+
attr_reader :schema_jsi
|
34
|
+
|
35
|
+
# @return [JSI::Base, JSI::JSON::Node] either a JSI::Base subclass or a
|
36
|
+
# JSI::JSON::Node for the schema
|
31
37
|
def schema_object
|
32
|
-
@
|
38
|
+
@schema_jsi || @schema_node
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [JSI::Base, JSI::JSON::Node, Object] property value from the schema_object
|
42
|
+
# @param property_name [String, Object] property name to access from the schema_object
|
43
|
+
def [](property_name)
|
44
|
+
schema_object[property_name]
|
33
45
|
end
|
34
46
|
|
47
|
+
# @return [String] an absolute id for the schema, with a json pointer fragment
|
35
48
|
def schema_id
|
36
49
|
@schema_id ||= begin
|
37
50
|
# start from schema_node and ascend parents looking for an 'id' property.
|
@@ -87,10 +100,17 @@ module JSI
|
|
87
100
|
end
|
88
101
|
end
|
89
102
|
|
103
|
+
# @return [Class subclassing JSI::Base] shortcut for JSI.class_for_schema(schema)
|
90
104
|
def schema_class
|
91
105
|
JSI.class_for_schema(self)
|
92
106
|
end
|
93
107
|
|
108
|
+
# if this schema is a oneOf, allOf, anyOf schema, #match_to_instance finds
|
109
|
+
# one of the subschemas that matches the given instance and returns it. if
|
110
|
+
# there are no matching *Of schemas, this schema is returned.
|
111
|
+
#
|
112
|
+
# @param instance [Object] the instance to which to attempt to match *Of subschemas
|
113
|
+
# @return [JSI::Schema] a matched subschema, or this schema (self)
|
94
114
|
def match_to_instance(instance)
|
95
115
|
# matching oneOf is good here. one schema for one instance.
|
96
116
|
# matching anyOf is okay. there could be more than one schema matched. it's often just one. if more
|
@@ -109,6 +129,10 @@ module JSI
|
|
109
129
|
return self
|
110
130
|
end
|
111
131
|
|
132
|
+
# @param property_name_ [String] the property for which to find a subschema
|
133
|
+
# @return [JSI::Schema, nil] a subschema from `properties`,
|
134
|
+
# `patternProperties`, or `additionalProperties` for the given
|
135
|
+
# property_name
|
112
136
|
def subschema_for_property(property_name_)
|
113
137
|
memoize(:subschema_for_property, property_name_) do |property_name|
|
114
138
|
if schema_object['properties'].respond_to?(:to_hash) && schema_object['properties'][property_name].respond_to?(:to_hash)
|
@@ -132,6 +156,9 @@ module JSI
|
|
132
156
|
end
|
133
157
|
end
|
134
158
|
|
159
|
+
# @param index_ [Integer] the index for which to find a subschema
|
160
|
+
# @return [JSI::Schema, nil] a subschema from `items` or
|
161
|
+
# `additionalItems` for the given index
|
135
162
|
def subschema_for_index(index_)
|
136
163
|
memoize(:subschema_for_index, index_) do |index|
|
137
164
|
if schema_object['items'].respond_to?(:to_ary)
|
@@ -148,44 +175,14 @@ module JSI
|
|
148
175
|
end
|
149
176
|
end
|
150
177
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
schema_node['minItems'] ||
|
160
|
-
schema_node.key?('uniqueItems') ||
|
161
|
-
schema_node['oneOf'].respond_to?(:to_ary) &&
|
162
|
-
schema_node['oneOf'].all? { |someof_node| self.class.new(someof_node).describes_array? } ||
|
163
|
-
schema_node['allOf'].respond_to?(:to_ary) &&
|
164
|
-
schema_node['allOf'].all? { |someof_node| self.class.new(someof_node).describes_array? } ||
|
165
|
-
schema_node['anyOf'].respond_to?(:to_ary) &&
|
166
|
-
schema_node['anyOf'].all? { |someof_node| self.class.new(someof_node).describes_array? }
|
167
|
-
end
|
168
|
-
end
|
169
|
-
def describes_hash?
|
170
|
-
memoize(:describes_hash?) do
|
171
|
-
schema_node['type'] == 'object' ||
|
172
|
-
schema_node['required'].respond_to?(:to_ary) ||
|
173
|
-
schema_node['properties'].respond_to?(:to_hash) ||
|
174
|
-
schema_node['additionalProperties'] ||
|
175
|
-
schema_node['patternProperties'] ||
|
176
|
-
schema_node['default'].respond_to?(:to_hash) ||
|
177
|
-
(schema_node['enum'].respond_to?(:to_ary) && schema_node['enum'].all? { |enum| enum.respond_to?(:to_hash) }) ||
|
178
|
-
schema_node['oneOf'].respond_to?(:to_ary) &&
|
179
|
-
schema_node['oneOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? } ||
|
180
|
-
schema_node['allOf'].respond_to?(:to_ary) &&
|
181
|
-
schema_node['allOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? } ||
|
182
|
-
schema_node['anyOf'].respond_to?(:to_ary) &&
|
183
|
-
schema_node['anyOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? }
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
def described_hash_property_names
|
188
|
-
memoize(:described_hash_property_names) do
|
178
|
+
# @return [Set] any object property names this schema indicates may be
|
179
|
+
# present on its instances. this includes, if present: keys of this
|
180
|
+
# schema's "properties" object; entries of this schema's array of
|
181
|
+
# "required" property keys. if this schema has oneOf/allOf/anyOf
|
182
|
+
# subschemas, those schemas are checked (recursively) for their
|
183
|
+
# described object property names.
|
184
|
+
def described_object_property_names
|
185
|
+
memoize(:described_object_property_names) do
|
189
186
|
Set.new.tap do |property_names|
|
190
187
|
if schema_node['properties'].respond_to?(:to_hash)
|
191
188
|
property_names.merge(schema_node['properties'].keys)
|
@@ -197,30 +194,65 @@ module JSI
|
|
197
194
|
# we should look at dependencies (TODO).
|
198
195
|
%w(oneOf allOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |schemas_key|
|
199
196
|
schema_node[schemas_key].map(&:deref).map do |someof_node|
|
200
|
-
property_names.merge(self.class.new(someof_node).
|
197
|
+
property_names.merge(self.class.new(someof_node).described_object_property_names)
|
201
198
|
end
|
202
199
|
end
|
203
200
|
end
|
204
201
|
end
|
205
202
|
end
|
206
203
|
|
204
|
+
# @return [Array<String>] array of schema validation error messages for
|
205
|
+
# the given instance against this schema
|
207
206
|
def fully_validate(instance)
|
208
|
-
::JSON::Validator.fully_validate(schema_node.document,
|
207
|
+
::JSON::Validator.fully_validate(JSI::Typelike.as_json(schema_node.document), JSI::Typelike.as_json(instance), fragment: schema_node.fragment)
|
209
208
|
end
|
209
|
+
|
210
|
+
# @return [true, false] whether the given instance validates against this schema
|
210
211
|
def validate(instance)
|
211
|
-
::JSON::Validator.validate(schema_node.document,
|
212
|
+
::JSON::Validator.validate(JSI::Typelike.as_json(schema_node.document), JSI::Typelike.as_json(instance), fragment: schema_node.fragment)
|
212
213
|
end
|
214
|
+
|
215
|
+
# @return [true] if this method does not raise, it returns true to
|
216
|
+
# indicate the instance is valid against this schema
|
217
|
+
# @raise [::JSON::Schema::ValidationError] raises if the instance has
|
218
|
+
# validation errors against this schema
|
213
219
|
def validate!(instance)
|
214
|
-
::JSON::Validator.validate!(schema_node.document,
|
220
|
+
::JSON::Validator.validate!(JSI::Typelike.as_json(schema_node.document), JSI::Typelike.as_json(instance), fragment: schema_node.fragment)
|
215
221
|
end
|
216
222
|
|
223
|
+
# @return [Array<String>] array of schema validation error messages for
|
224
|
+
# this schema, validated against its metaschema. a default metaschema
|
225
|
+
# is assumed if the schema does not specify a $schema.
|
226
|
+
def fully_validate_schema
|
227
|
+
::JSON::Validator.fully_validate(JSI::Typelike.as_json(schema_node.document), [], fragment: schema_node.fragment, validate_schema: true, list: true)
|
228
|
+
end
|
229
|
+
|
230
|
+
# @return [true, false] whether this schema validates against its metaschema
|
231
|
+
def validate_schema
|
232
|
+
::JSON::Validator.validate(JSI::Typelike.as_json(schema_node.document), [], fragment: schema_node.fragment, validate_schema: true, list: true)
|
233
|
+
end
|
234
|
+
|
235
|
+
# @return [true] if this method does not raise, it returns true to
|
236
|
+
# indicate this schema is valid against its metaschema
|
237
|
+
# @raise [::JSON::Schema::ValidationError] raises if this schema has
|
238
|
+
# validation errors against its metaschema
|
239
|
+
def validate_schema!
|
240
|
+
::JSON::Validator.validate!(JSI::Typelike.as_json(schema_node.document), [], fragment: schema_node.fragment, validate_schema: true, list: true)
|
241
|
+
end
|
242
|
+
|
243
|
+
# @return [String] a string for #instance and #pretty_print including the schema_id
|
217
244
|
def object_group_text
|
218
245
|
"schema_id=#{schema_id}"
|
219
246
|
end
|
247
|
+
|
248
|
+
# @return [String] a string representing this Schema
|
220
249
|
def inspect
|
221
250
|
"\#<#{self.class.inspect} #{object_group_text} #{schema_object.inspect}>"
|
222
251
|
end
|
223
252
|
alias_method :to_s, :inspect
|
253
|
+
|
254
|
+
# pretty-prints a representation this Schema to the given printer
|
255
|
+
# @return [void]
|
224
256
|
def pretty_print(q)
|
225
257
|
q.instance_exec(self) do |obj|
|
226
258
|
text "\#<#{obj.class.inspect} #{obj.object_group_text}"
|
@@ -234,16 +266,16 @@ module JSI
|
|
234
266
|
text '>'
|
235
267
|
end
|
236
268
|
end
|
269
|
+
|
270
|
+
# @return [Object] returns a jsonifiable representation of this schema
|
271
|
+
def as_json(*opt)
|
272
|
+
Typelike.as_json(schema_object, *opt)
|
273
|
+
end
|
274
|
+
|
275
|
+
# @return [Object] an opaque fingerprint of this Schema for FingerprintHash
|
237
276
|
def fingerprint
|
238
277
|
{class: self.class, schema_node: schema_node}
|
239
278
|
end
|
240
279
|
include FingerprintHash
|
241
|
-
|
242
|
-
private
|
243
|
-
def object_to_content(object)
|
244
|
-
object = object.instance if object.is_a?(JSI::Base)
|
245
|
-
object = object.content if object.is_a?(JSI::JSON::Node)
|
246
|
-
object
|
247
|
-
end
|
248
280
|
end
|
249
281
|
end
|