jsi 0.1.0 → 0.2.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/CHANGELOG.md +20 -0
- data/README.md +18 -15
- data/jsi.gemspec +1 -1
- data/lib/jsi.rb +5 -1
- data/lib/jsi/base.rb +222 -215
- data/lib/jsi/json-schema-fragments.rb +1 -1
- data/lib/jsi/json/node.rb +61 -146
- data/lib/jsi/json/pointer.rb +235 -41
- data/lib/jsi/pathed_node.rb +113 -0
- data/lib/jsi/schema.rb +46 -40
- data/lib/jsi/schema_classes.rb +86 -0
- data/lib/jsi/simple_wrap.rb +7 -0
- data/lib/jsi/typelike_modules.rb +39 -11
- data/lib/jsi/util.rb +3 -0
- data/lib/jsi/version.rb +1 -1
- data/test/base_array_test.rb +63 -51
- data/test/base_hash_test.rb +38 -28
- data/test/base_test.rb +54 -27
- data/test/jsi_json_arraynode_test.rb +19 -18
- data/test/jsi_json_hashnode_test.rb +29 -28
- data/test/jsi_json_node_test.rb +50 -28
- data/test/jsi_json_pointer_test.rb +13 -5
- data/test/schema_test.rb +13 -13
- data/test/spreedly_openapi_test.rb +8 -0
- data/test/test_helper.rb +3 -3
- data/test/util_test.rb +10 -10
- metadata +8 -3
@@ -43,7 +43,7 @@ module JSON
|
|
43
43
|
def schema_from_fragment(base_schema, fragment)
|
44
44
|
schema_uri = base_schema.uri
|
45
45
|
|
46
|
-
pointer = JSI::JSON::Pointer.
|
46
|
+
pointer = JSI::JSON::Pointer.from_fragment(fragment)
|
47
47
|
|
48
48
|
base_schema = JSON::Schema.new(pointer.evaluate(base_schema.schema), schema_uri, @options[:version])
|
49
49
|
|
data/lib/jsi/json/node.rb
CHANGED
@@ -22,52 +22,56 @@ module JSI
|
|
22
22
|
# return a copy of the document with the content of the node modified.
|
23
23
|
# the original node's document and content are untouched.
|
24
24
|
class Node
|
25
|
+
include PathedNode
|
26
|
+
|
25
27
|
def self.new_doc(document)
|
26
|
-
new_by_type(document, [])
|
28
|
+
new_by_type(document, JSI::JSON::Pointer.new([]))
|
27
29
|
end
|
28
30
|
|
29
|
-
# if the content of the document at the given
|
31
|
+
# if the content of the document at the given pointer is Hash-like, returns
|
30
32
|
# a HashNode; if Array-like, returns ArrayNode. otherwise returns a
|
31
33
|
# regular Node, although Nodes are for the most part instantiated from
|
32
34
|
# Hash or Array-like content.
|
33
|
-
def self.new_by_type(document,
|
34
|
-
|
35
|
-
content = node.content
|
35
|
+
def self.new_by_type(document, pointer)
|
36
|
+
content = pointer.evaluate(document)
|
36
37
|
if content.respond_to?(:to_hash)
|
37
|
-
HashNode.new(document,
|
38
|
+
HashNode.new(document, pointer)
|
38
39
|
elsif content.respond_to?(:to_ary)
|
39
|
-
ArrayNode.new(document,
|
40
|
+
ArrayNode.new(document, pointer)
|
40
41
|
else
|
41
|
-
|
42
|
+
Node.new(document, pointer)
|
42
43
|
end
|
43
44
|
end
|
44
45
|
|
45
|
-
# a Node represents the content of a document at a given
|
46
|
-
def initialize(document,
|
47
|
-
unless
|
48
|
-
raise(
|
46
|
+
# a Node represents the content of a document at a given pointer.
|
47
|
+
def initialize(document, pointer)
|
48
|
+
unless pointer.is_a?(JSI::JSON::Pointer)
|
49
|
+
raise(TypeError, "pointer must be a JSI::JSON::Pointer. got: #{pointer.pretty_inspect.chomp} (#{pointer.class})")
|
49
50
|
end
|
50
51
|
if document.is_a?(JSI::JSON::Node)
|
51
52
|
raise(TypeError, "document of a Node should not be another JSI::JSON::Node: #{document.inspect}")
|
52
53
|
end
|
53
54
|
@document = document
|
54
|
-
@
|
55
|
-
@pointer = JSI::JSON::Pointer.new(:reference_tokens, path)
|
55
|
+
@pointer = pointer
|
56
56
|
end
|
57
57
|
|
58
|
-
# the
|
59
|
-
attr_reader :path
|
60
|
-
# the document containing this Node at is path
|
58
|
+
# the document containing this Node at our pointer
|
61
59
|
attr_reader :document
|
62
|
-
|
60
|
+
|
61
|
+
# JSI::JSON::Pointer pointing to this node within its document
|
63
62
|
attr_reader :pointer
|
64
63
|
|
65
|
-
# the
|
66
|
-
def
|
67
|
-
|
68
|
-
content
|
64
|
+
# @return [Array<Object>] the path of this node; an array of reference_tokens of the pointer
|
65
|
+
def path
|
66
|
+
pointer.reference_tokens
|
69
67
|
end
|
70
68
|
|
69
|
+
alias_method :node_document, :document
|
70
|
+
alias_method :node_ptr, :pointer
|
71
|
+
|
72
|
+
# the raw content of this Node from the underlying document at this Node's pointer.
|
73
|
+
alias_method :content, :node_content
|
74
|
+
|
71
75
|
# returns content at the given subscript - call this the subcontent.
|
72
76
|
#
|
73
77
|
# if the content cannot be subscripted, raises TypeError.
|
@@ -78,11 +82,13 @@ module JSI
|
|
78
82
|
# if this node's content is a $ref - that is, a hash with a $ref attribute - and the subscript is
|
79
83
|
# not a key of the hash, then the $ref is followed before returning the subcontent.
|
80
84
|
def [](subscript)
|
81
|
-
|
82
|
-
content =
|
85
|
+
ptr = self.pointer
|
86
|
+
content = self.content
|
83
87
|
if content.respond_to?(:to_hash) && !(content.respond_to?(:key?) ? content : content.to_hash).key?(subscript)
|
84
|
-
|
85
|
-
|
88
|
+
pointer.deref(document) do |deref_ptr|
|
89
|
+
ptr = deref_ptr
|
90
|
+
content = ptr.evaluate(document)
|
91
|
+
end
|
86
92
|
end
|
87
93
|
unless content.respond_to?(:[])
|
88
94
|
if content.respond_to?(:to_hash)
|
@@ -99,9 +105,9 @@ module JSI
|
|
99
105
|
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)
|
100
106
|
end
|
101
107
|
if subcontent.respond_to?(:to_hash)
|
102
|
-
HashNode.new(
|
108
|
+
HashNode.new(document, ptr[subscript])
|
103
109
|
elsif subcontent.respond_to?(:to_ary)
|
104
|
-
ArrayNode.new(
|
110
|
+
ArrayNode.new(document, ptr[subscript])
|
105
111
|
else
|
106
112
|
subcontent
|
107
113
|
end
|
@@ -120,30 +126,15 @@ module JSI
|
|
120
126
|
# does not have a $ref, or if what its $ref cannot be found, this node is returned.
|
121
127
|
#
|
122
128
|
# currently only $refs pointing within the same document are followed.
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
if ref[/\A#/]
|
132
|
-
return self.class.new_by_type(document, JSI::JSON::Pointer.parse_fragment(ref)).deref
|
133
|
-
end
|
134
|
-
|
135
|
-
# HAX for how google does refs and ids
|
136
|
-
if document_node['schemas'].respond_to?(:to_hash)
|
137
|
-
if document_node['schemas'][ref]
|
138
|
-
return document_node['schemas'][ref]
|
139
|
-
end
|
140
|
-
_, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == ref }
|
141
|
-
if deref_by_id
|
142
|
-
return deref_by_id
|
143
|
-
end
|
129
|
+
#
|
130
|
+
# @yield [Node] if a block is given (optional), this will yield a deref'd node. if this
|
131
|
+
# node is not a $ref object, the block is not called. if we are a $ref which cannot be followed
|
132
|
+
# (e.g. a $ref to an external document, which is not yet supported), the block is not called.
|
133
|
+
# @return [JSI::JSON::Node] dereferenced node, or this node
|
134
|
+
def deref(&block)
|
135
|
+
pointer.deref(document) do |deref_ptr|
|
136
|
+
return Node.new_by_type(document, deref_ptr).tap(&(block || Util::NOOP))
|
144
137
|
end
|
145
|
-
|
146
|
-
#raise(NotImplementedError, "cannot dereference #{ref}") # TODO
|
147
138
|
return self
|
148
139
|
end
|
149
140
|
|
@@ -152,14 +143,17 @@ module JSI
|
|
152
143
|
Node.new_doc(document)
|
153
144
|
end
|
154
145
|
|
155
|
-
|
146
|
+
alias_method :document_root_node, :document_node
|
147
|
+
|
148
|
+
# @return [Boolean] whether this node is the root of its document
|
149
|
+
def root_node?
|
150
|
+
pointer.root?
|
151
|
+
end
|
152
|
+
|
153
|
+
# the parent of this node. if this node is the document root, raises
|
156
154
|
# JSI::JSON::Pointer::ReferenceError.
|
157
155
|
def parent_node
|
158
|
-
|
159
|
-
raise(JSI::JSON::Pointer::ReferenceError, "cannot access parent of root node: #{pretty_inspect.chomp}")
|
160
|
-
else
|
161
|
-
Node.new_by_type(document, path[0...-1])
|
162
|
-
end
|
156
|
+
Node.new_by_type(document, pointer.parent)
|
163
157
|
end
|
164
158
|
|
165
159
|
# the pointer path to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
|
@@ -179,52 +173,12 @@ module JSI
|
|
179
173
|
|
180
174
|
# takes a block. the block is yielded the content of this node. the block MUST return a modified
|
181
175
|
# copy of that content (and NOT modify the object it is given).
|
182
|
-
def modified_copy
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
# that is recursively merged up to the document root. the recursion is done with a
|
189
|
-
# y combinator, for no other reason than that was a fun way to implement it.
|
190
|
-
modified_document = JSI::Util.ycomb do |rec|
|
191
|
-
proc do |subdocument, subpath|
|
192
|
-
if subpath == []
|
193
|
-
yield(subdocument)
|
194
|
-
else
|
195
|
-
car = subpath[0]
|
196
|
-
cdr = subpath[1..-1]
|
197
|
-
if subdocument.respond_to?(:to_hash)
|
198
|
-
subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_hash)[car]
|
199
|
-
car_object = rec.call(subdocument_car, cdr)
|
200
|
-
if car_object.object_id == subdocument_car.object_id
|
201
|
-
subdocument
|
202
|
-
else
|
203
|
-
(subdocument.respond_to?(:merge) ? subdocument : subdocument.to_hash).merge({car => car_object})
|
204
|
-
end
|
205
|
-
elsif subdocument.respond_to?(:to_ary)
|
206
|
-
if car.is_a?(String) && car =~ /\A\d+\z/
|
207
|
-
car = car.to_i
|
208
|
-
end
|
209
|
-
unless car.is_a?(Integer)
|
210
|
-
raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
|
211
|
-
end
|
212
|
-
subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_ary)[car]
|
213
|
-
car_object = rec.call(subdocument_car, cdr)
|
214
|
-
if car_object.object_id == subdocument_car.object_id
|
215
|
-
subdocument
|
216
|
-
else
|
217
|
-
(subdocument.respond_to?(:[]=) ? subdocument : subdocument.to_ary).dup.tap do |arr|
|
218
|
-
arr[car] = car_object
|
219
|
-
end
|
220
|
-
end
|
221
|
-
else
|
222
|
-
raise(TypeError, "bad subscript: #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect.chomp}")
|
223
|
-
end
|
224
|
-
end
|
225
|
-
end
|
226
|
-
end.call(document, path)
|
227
|
-
Node.new_by_type(modified_document, path)
|
176
|
+
def modified_copy(&block)
|
177
|
+
Node.new_by_type(pointer.modified_document_copy(document, &block), pointer)
|
178
|
+
end
|
179
|
+
|
180
|
+
def dup
|
181
|
+
modified_copy(&:dup)
|
228
182
|
end
|
229
183
|
|
230
184
|
# meta-information about the object, outside the content. used by #inspect / #pretty_print
|
@@ -254,10 +208,10 @@ module JSI
|
|
254
208
|
|
255
209
|
# fingerprint for equality (see FingerprintHash). two nodes are equal if they are both nodes
|
256
210
|
# (regardless of type, e.g. one may be a Node and the other may be a HashNode) within equal
|
257
|
-
# documents at equal
|
211
|
+
# documents at equal pointers. note that this means two nodes with the same content may not be
|
258
212
|
# considered equal.
|
259
213
|
def fingerprint
|
260
|
-
{
|
214
|
+
{class: JSI::JSON::Node, document: document, pointer: pointer}
|
261
215
|
end
|
262
216
|
include FingerprintHash
|
263
217
|
end
|
@@ -265,64 +219,25 @@ module JSI
|
|
265
219
|
# a JSI::JSON::Node whose content is Array-like (responds to #to_ary)
|
266
220
|
# and includes Array methods from Arraylike
|
267
221
|
class ArrayNode < Node
|
268
|
-
# iterates over each element in the same manner as Array#each
|
269
|
-
def each
|
270
|
-
return to_enum(__method__) { (content.respond_to?(:size) ? content : content.to_ary).size } unless block_given?
|
271
|
-
(content.respond_to?(:each_index) ? content : content.to_ary).each_index { |i| yield self[i] }
|
272
|
-
self
|
273
|
-
end
|
274
|
-
|
275
|
-
# the content of this ArrayNode, as an Array
|
276
|
-
def to_ary
|
277
|
-
to_a
|
278
|
-
end
|
279
|
-
|
280
222
|
include Enumerable
|
281
|
-
include
|
223
|
+
include PathedArrayNode
|
282
224
|
|
283
225
|
# returns a jsonifiable representation of this node's content
|
284
226
|
def as_json(*opt) # needs redefined after including Enumerable
|
285
227
|
Typelike.as_json(content, *opt)
|
286
228
|
end
|
287
|
-
|
288
|
-
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
|
289
|
-
# we override these methods from Arraylike
|
290
|
-
SAFE_INDEX_ONLY_METHODS.each do |method_name|
|
291
|
-
define_method(method_name) { |*a, &b| (content.respond_to?(method_name) ? content : content.to_ary).public_send(method_name, *a, &b) }
|
292
|
-
end
|
293
229
|
end
|
294
230
|
|
295
231
|
# a JSI::JSON::Node whose content is Hash-like (responds to #to_hash)
|
296
232
|
# and includes Hash methods from Hashlike
|
297
233
|
class HashNode < Node
|
298
|
-
# iterates over each element in the same manner as Array#each
|
299
|
-
def each(&block)
|
300
|
-
return to_enum(__method__) { content.respond_to?(:size) ? content.size : content.to_ary.size } unless block_given?
|
301
|
-
if block.arity > 1
|
302
|
-
(content.respond_to?(:each_key) ? content : content.to_hash).each_key { |k| yield k, self[k] }
|
303
|
-
else
|
304
|
-
(content.respond_to?(:each_key) ? content : content.to_hash).each_key { |k| yield [k, self[k]] }
|
305
|
-
end
|
306
|
-
self
|
307
|
-
end
|
308
|
-
|
309
|
-
# the content of this HashNode, as a Hash
|
310
|
-
def to_hash
|
311
|
-
inject({}) { |h, (k, v)| h[k] = v; h }
|
312
|
-
end
|
313
|
-
|
314
234
|
include Enumerable
|
315
|
-
include
|
235
|
+
include PathedHashNode
|
316
236
|
|
317
237
|
# returns a jsonifiable representation of this node's content
|
318
238
|
def as_json(*opt) # needs redefined after including Enumerable
|
319
239
|
Typelike.as_json(content, *opt)
|
320
240
|
end
|
321
|
-
|
322
|
-
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
|
323
|
-
SAFE_KEY_ONLY_METHODS.each do |method_name|
|
324
|
-
define_method(method_name) { |*a, &b| (content.respond_to?(method_name) ? content : content.to_hash).public_send(method_name, *a, &b) }
|
325
|
-
end
|
326
241
|
end
|
327
242
|
end
|
328
243
|
end
|
data/lib/jsi/json/pointer.rb
CHANGED
@@ -11,66 +11,88 @@ module JSI
|
|
11
11
|
class ReferenceError < Error
|
12
12
|
end
|
13
13
|
|
14
|
-
#
|
14
|
+
# instantiates a Pointer from any given reference tokens.
|
15
15
|
#
|
16
|
-
#
|
16
|
+
# >> JSI::JSON::Pointer[]
|
17
|
+
# => #<JSI::JSON::Pointer reference_tokens: []>
|
18
|
+
# >> JSI::JSON::Pointer['a', 'b']
|
19
|
+
# => #<JSI::JSON::Pointer reference_tokens: ["a", "b"]>
|
20
|
+
# >> JSI::JSON::Pointer['a']['b']
|
21
|
+
# => #<JSI::JSON::Pointer reference_tokens: ["a", "b"]>
|
17
22
|
#
|
18
|
-
#
|
23
|
+
# note in the last example that you can conveniently chain the class .[] method
|
24
|
+
# with the instance #[] method.
|
19
25
|
#
|
20
|
-
#
|
26
|
+
# @param *reference_tokens any number of reference tokens
|
27
|
+
# @return [JSI::JSON::Pointer]
|
28
|
+
def self.[](*reference_tokens)
|
29
|
+
new(reference_tokens)
|
30
|
+
end
|
31
|
+
|
32
|
+
# parse a URI-escaped fragment and instantiate as a JSI::JSON::Pointer
|
33
|
+
#
|
34
|
+
# ptr = JSI::JSON::Pointer.from_fragment('#/foo/bar')
|
35
|
+
# => #<JSI::JSON::Pointer fragment: #/foo/bar>
|
36
|
+
# ptr.reference_tokens
|
37
|
+
# => ["foo", "bar"]
|
38
|
+
#
|
39
|
+
# with URI escaping:
|
21
40
|
#
|
22
|
-
#
|
23
|
-
|
41
|
+
# ptr = JSI::JSON::Pointer.from_fragment('#/foo%20bar')
|
42
|
+
# => #<JSI::JSON::Pointer fragment: #/foo%20bar>
|
43
|
+
# ptr.reference_tokens
|
44
|
+
# => ["foo bar"]
|
45
|
+
#
|
46
|
+
# @param fragment [String] a fragment containing a pointer (starting with #)
|
47
|
+
# @return [JSI::JSON::Pointer]
|
48
|
+
def self.from_fragment(fragment)
|
24
49
|
fragment = Addressable::URI.unescape(fragment)
|
25
50
|
match = fragment.match(/\A#/)
|
26
51
|
if match
|
27
|
-
|
52
|
+
from_pointer(match.post_match, type: 'fragment')
|
28
53
|
else
|
29
54
|
raise(PointerSyntaxError, "Invalid fragment syntax in #{fragment.inspect}: fragment must begin with #")
|
30
55
|
end
|
31
56
|
end
|
32
57
|
|
33
|
-
# parse a pointer
|
34
|
-
#
|
35
|
-
# /foo
|
58
|
+
# parse a pointer string and instantiate as a JSI::JSON::Pointer
|
36
59
|
#
|
37
|
-
#
|
60
|
+
# ptr1 = JSI::JSON::Pointer.from_pointer('/foo')
|
61
|
+
# => #<JSI::JSON::Pointer pointer: /foo>
|
62
|
+
# ptr1.reference_tokens
|
63
|
+
# => ["foo"]
|
38
64
|
#
|
39
|
-
# /foo~0bar/baz~1qux
|
65
|
+
# ptr2 = JSI::JSON::Pointer.from_pointer('/foo~0bar/baz~1qux')
|
66
|
+
# => #<JSI::JSON::Pointer pointer: /foo~0bar/baz~1qux>
|
67
|
+
# ptr2.reference_tokens
|
68
|
+
# => ["foo~bar", "baz/qux"]
|
40
69
|
#
|
41
|
-
#
|
42
|
-
|
70
|
+
# @param pointer_string [String] a pointer string
|
71
|
+
# @param type (for internal use) indicates the original representation of the pointer
|
72
|
+
# @return [JSI::JSON::Pointer]
|
73
|
+
def self.from_pointer(pointer_string, type: 'pointer')
|
43
74
|
tokens = pointer_string.split('/', -1).map! do |piece|
|
44
75
|
piece.gsub('~1', '/').gsub('~0', '~')
|
45
76
|
end
|
46
77
|
if tokens[0] == ''
|
47
|
-
tokens[1..-1]
|
78
|
+
new(tokens[1..-1], type: type)
|
48
79
|
elsif tokens.empty?
|
49
|
-
tokens
|
80
|
+
new(tokens, type: type)
|
50
81
|
else
|
51
82
|
raise(PointerSyntaxError, "Invalid pointer syntax in #{pointer_string.inspect}: pointer must begin with /")
|
52
83
|
end
|
53
84
|
end
|
54
85
|
|
55
|
-
# initializes a JSI::JSON::Pointer from the given
|
56
|
-
#
|
57
|
-
# type may be one of:
|
86
|
+
# initializes a JSI::JSON::Pointer from the given reference_tokens.
|
58
87
|
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
if type == :reference_tokens
|
65
|
-
reference_tokens = representation
|
66
|
-
elsif type == :fragment
|
67
|
-
reference_tokens = self.class.parse_fragment(representation)
|
68
|
-
elsif type == :pointer
|
69
|
-
reference_tokens = self.class.parse_pointer(representation)
|
70
|
-
else
|
71
|
-
raise ArgumentError, "invalid initialization type: #{type.inspect} with representation #{representation.inspect}"
|
88
|
+
# @param reference_tokens [Array<Object>]
|
89
|
+
# @param type [String, Symbol] one of 'pointer' or 'fragment'
|
90
|
+
def initialize(reference_tokens, type: nil)
|
91
|
+
unless reference_tokens.respond_to?(:to_ary)
|
92
|
+
raise(TypeError, "reference_tokens must be an array. got: #{reference_tokens.inspect}")
|
72
93
|
end
|
73
|
-
@reference_tokens = reference_tokens.map(&:freeze).freeze
|
94
|
+
@reference_tokens = reference_tokens.to_ary.map(&:freeze).freeze
|
95
|
+
@type = type.is_a?(Symbol) ? type.to_s : type
|
74
96
|
end
|
75
97
|
|
76
98
|
attr_reader :reference_tokens
|
@@ -106,29 +128,201 @@ module JSI
|
|
106
128
|
res
|
107
129
|
end
|
108
130
|
|
109
|
-
# the pointer string representation of this Pointer
|
131
|
+
# @return [String] the pointer string representation of this Pointer
|
110
132
|
def pointer
|
111
133
|
reference_tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('')
|
112
134
|
end
|
113
135
|
|
114
|
-
# the fragment string representation of this Pointer
|
136
|
+
# @return [String] the fragment string representation of this Pointer
|
115
137
|
def fragment
|
116
138
|
'#' + Addressable::URI.escape(pointer)
|
117
139
|
end
|
118
140
|
|
141
|
+
# @return [Boolean] whether this pointer points to the root (has an empty array of reference_tokens)
|
142
|
+
def root?
|
143
|
+
reference_tokens.empty?
|
144
|
+
end
|
145
|
+
|
146
|
+
# @return [JSI::JSON::Pointer] pointer to the parent of where this pointer points
|
147
|
+
# @raise [JSI::JSON::Pointer::ReferenceError] if this pointer has no parent (points to the root)
|
148
|
+
def parent
|
149
|
+
if root?
|
150
|
+
raise(ReferenceError, "cannot access parent of root pointer: #{pretty_inspect.chomp}")
|
151
|
+
else
|
152
|
+
Pointer.new(reference_tokens[0...-1], type: @type)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# @return [Boolean] does this pointer contain the other_ptr - that is, is this pointer an
|
157
|
+
# ancestor of other_ptr, a child pointer. contains? is inclusive; a pointer does contain itself.
|
158
|
+
def contains?(other_ptr)
|
159
|
+
self.reference_tokens == other_ptr.reference_tokens[0...self.reference_tokens.size]
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [JSI::JSON::Pointer] returns this pointer relative to the given ancestor_ptr
|
163
|
+
# @raise [JSI::JSON::Pointer::ReferenceError] if the given ancestor_ptr is not an ancestor of this pointer
|
164
|
+
def ptr_relative_to(ancestor_ptr)
|
165
|
+
unless ancestor_ptr.contains?(self)
|
166
|
+
raise(ReferenceError, "ancestor_ptr #{ancestor_ptr.inspect} is not ancestor of #{inspect}")
|
167
|
+
end
|
168
|
+
Pointer.new(reference_tokens[ancestor_ptr.reference_tokens.size..-1], type: @type)
|
169
|
+
end
|
170
|
+
|
171
|
+
# @param ptr [JSI::JSON::Pointer]
|
172
|
+
# @return [JSI::JSON::Pointer] a pointer with the reference tokens of this one plus the given ptr's.
|
173
|
+
def +(ptr)
|
174
|
+
unless ptr.is_a?(JSI::JSON::Pointer)
|
175
|
+
raise(TypeError, "ptr must be a JSI::JSON::Pointer; got: #{ptr.inspect}")
|
176
|
+
end
|
177
|
+
Pointer.new(reference_tokens + ptr.reference_tokens, type: @type)
|
178
|
+
end
|
179
|
+
|
180
|
+
# @param n [Integer]
|
181
|
+
# @return [JSI::JSON::Pointer] a Pointer consisting of the first n of our reference_tokens
|
182
|
+
# @raise [ArgumentError] if n is not between 0 and the size of our reference_tokens
|
183
|
+
def take(n)
|
184
|
+
unless (0..reference_tokens.size).include?(n)
|
185
|
+
raise(ArgumentError, "n not in range (0..#{reference_tokens.size}): #{n.inspect}")
|
186
|
+
end
|
187
|
+
Pointer.new(reference_tokens.take(n), type: @type)
|
188
|
+
end
|
189
|
+
|
190
|
+
# appends the given token to this Pointer's reference tokens and returns the result
|
191
|
+
#
|
192
|
+
# @param token [Object]
|
193
|
+
# @return [JSI::JSON::Pointer] pointer to a child node of this pointer with the given token
|
194
|
+
def [](token)
|
195
|
+
Pointer.new(reference_tokens + [token], type: @type)
|
196
|
+
end
|
197
|
+
|
198
|
+
# takes a document and a block. the block is yielded the content of the given document at this
|
199
|
+
# pointer's location. the block must result a modified copy of that content (and MUST NOT modify
|
200
|
+
# the object it is given). this modified copy of that content is incorporated into a modified copy
|
201
|
+
# of the given document, which is then returned. the structure and contents of the document outside
|
202
|
+
# the path pointed to by this pointer is not modified.
|
203
|
+
#
|
204
|
+
# @param document [Object] the document to apply this pointer to
|
205
|
+
# @yield [Object] the content this pointer applies to in the given document
|
206
|
+
# the block must result in the new content which will be placed in the modified document copy.
|
207
|
+
# @return [Object] a copy of the given document, with the content this pointer applies to
|
208
|
+
# replaced by the result of the block
|
209
|
+
def modified_document_copy(document, &block)
|
210
|
+
# we need to preserve the rest of the document, but modify the content at our path.
|
211
|
+
#
|
212
|
+
# this is actually a bit tricky. we can't modify the original document, obviously.
|
213
|
+
# we could do a deep copy, but that's expensive. instead, we make a copy of each array
|
214
|
+
# or hash in the path above this node. this node's content is modified by the caller, and
|
215
|
+
# that is recursively merged up to the document root. the recursion is done with a
|
216
|
+
# y combinator, for no other reason than that was a fun way to implement it.
|
217
|
+
modified_document = JSI::Util.ycomb do |rec|
|
218
|
+
proc do |subdocument, subpath|
|
219
|
+
if subpath == []
|
220
|
+
Typelike.modified_copy(subdocument, &block)
|
221
|
+
else
|
222
|
+
car = subpath[0]
|
223
|
+
cdr = subpath[1..-1]
|
224
|
+
if subdocument.respond_to?(:to_hash)
|
225
|
+
subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_hash)[car]
|
226
|
+
car_object = rec.call(subdocument_car, cdr)
|
227
|
+
if car_object.object_id == subdocument_car.object_id
|
228
|
+
subdocument
|
229
|
+
else
|
230
|
+
(subdocument.respond_to?(:merge) ? subdocument : subdocument.to_hash).merge({car => car_object})
|
231
|
+
end
|
232
|
+
elsif subdocument.respond_to?(:to_ary)
|
233
|
+
if car.is_a?(String) && car =~ /\A\d+\z/
|
234
|
+
car = car.to_i
|
235
|
+
end
|
236
|
+
unless car.is_a?(Integer)
|
237
|
+
raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
|
238
|
+
end
|
239
|
+
subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_ary)[car]
|
240
|
+
car_object = rec.call(subdocument_car, cdr)
|
241
|
+
if car_object.object_id == subdocument_car.object_id
|
242
|
+
subdocument
|
243
|
+
else
|
244
|
+
(subdocument.respond_to?(:[]=) ? subdocument : subdocument.to_ary).dup.tap do |arr|
|
245
|
+
arr[car] = car_object
|
246
|
+
end
|
247
|
+
end
|
248
|
+
else
|
249
|
+
raise(TypeError, "bad subscript: #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect.chomp}")
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end.call(document, reference_tokens)
|
254
|
+
modified_document
|
255
|
+
end
|
256
|
+
|
257
|
+
# if this Pointer points at a $ref node within the given document, #deref attempts
|
258
|
+
# to follow that $ref and return a Pointer to the referenced location. otherwise,
|
259
|
+
# this Pointer is returned.
|
260
|
+
#
|
261
|
+
# if the content this Pointer points to in the document is not hash-like, does not
|
262
|
+
# have a $ref property, its $ref cannot be found, or its $ref points outside the document,
|
263
|
+
# this pointer is returned.
|
264
|
+
#
|
265
|
+
# @param document [Object] the document this pointer applies to
|
266
|
+
# @yield [Pointer] if a block is given (optional), this will yield a deref'd pointer. if this
|
267
|
+
# pointer does not point to a $ref object in the given document, the block is not called.
|
268
|
+
# if we point to a $ref which cannot be followed (e.g. a $ref to an external
|
269
|
+
# document, which is not yet supported), the block is not called.
|
270
|
+
# @return [Pointer] dereferenced pointer, or this pointer
|
271
|
+
def deref(document, &block)
|
272
|
+
block ||= Util::NOOP
|
273
|
+
content = evaluate(document)
|
274
|
+
|
275
|
+
if content.respond_to?(:to_hash)
|
276
|
+
ref = (content.respond_to?(:[]) ? content : content.to_hash)['$ref']
|
277
|
+
end
|
278
|
+
return self unless ref.is_a?(String)
|
279
|
+
|
280
|
+
if ref[/\A#/]
|
281
|
+
return Pointer.from_fragment(ref).tap(&block)
|
282
|
+
end
|
283
|
+
|
284
|
+
# HAX for how google does refs and ids
|
285
|
+
if document['schemas'].respond_to?(:to_hash)
|
286
|
+
if document['schemas'][ref]
|
287
|
+
return Pointer.new(['schemas', ref], type: 'hax').tap(&block)
|
288
|
+
end
|
289
|
+
document['schemas'].each do |k, schema|
|
290
|
+
if schema['id'] == ref
|
291
|
+
return Pointer.new(['schemas', k], type: 'hax').tap(&block)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
#raise(NotImplementedError, "cannot dereference #{ref}") # TODO
|
297
|
+
return self
|
298
|
+
end
|
299
|
+
|
300
|
+
# @return [String] string representation of this Pointer
|
301
|
+
def inspect
|
302
|
+
"#<#{self.class.inspect} #{representation_s}>"
|
303
|
+
end
|
304
|
+
|
305
|
+
# @return [String] string representation of this Pointer
|
119
306
|
def to_s
|
120
|
-
|
307
|
+
inspect
|
308
|
+
end
|
309
|
+
|
310
|
+
# pointers are equal if the reference_tokens are equal, regardless of @type
|
311
|
+
def fingerprint
|
312
|
+
{class: JSI::JSON::Pointer, reference_tokens: reference_tokens}
|
121
313
|
end
|
314
|
+
include FingerprintHash
|
122
315
|
|
123
316
|
private
|
124
317
|
|
318
|
+
# @return [String] a representation of this pointer based on @type
|
125
319
|
def representation_s
|
126
|
-
if @type ==
|
127
|
-
fragment
|
128
|
-
elsif @type ==
|
129
|
-
pointer
|
320
|
+
if @type == 'fragment'
|
321
|
+
"fragment: #{fragment}"
|
322
|
+
elsif @type == 'pointer'
|
323
|
+
"pointer: #{pointer}"
|
130
324
|
else
|
131
|
-
reference_tokens.inspect
|
325
|
+
"reference_tokens: #{reference_tokens.inspect}"
|
132
326
|
end
|
133
327
|
end
|
134
328
|
end
|