jsi 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|