jsi 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.new(:fragment, fragment)
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
 
@@ -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 path is Hash-like, returns
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, path)
34
- node = Node.new(document, path)
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, path)
38
+ HashNode.new(document, pointer)
38
39
  elsif content.respond_to?(:to_ary)
39
- ArrayNode.new(document, path)
40
+ ArrayNode.new(document, pointer)
40
41
  else
41
- node
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 path.
46
- def initialize(document, path)
47
- unless path.respond_to?(:to_ary)
48
- raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect.chomp} (#{path.class})")
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
- @path = path.to_ary.dup.freeze
55
- @pointer = JSI::JSON::Pointer.new(:reference_tokens, path)
55
+ @pointer = pointer
56
56
  end
57
57
 
58
- # the path of this Node within its document
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
- # JSI::JSON::Pointer representing the path to this node within its document
60
+
61
+ # JSI::JSON::Pointer pointing to this node within its document
63
62
  attr_reader :pointer
64
63
 
65
- # the raw content of this Node from the underlying document at this Node's path.
66
- def content
67
- content = pointer.evaluate(document)
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
- node = self
82
- content = node.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
- node = node.deref
85
- content = node.content
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(node.document, node.path + [subscript])
108
+ HashNode.new(document, ptr[subscript])
103
109
  elsif subcontent.respond_to?(:to_ary)
104
- ArrayNode.new(node.document, node.path + [subscript])
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
- def deref
124
- content = self.content
125
-
126
- if content.respond_to?(:to_hash)
127
- ref = (content.respond_to?(:[]) ? content : content.to_hash)['$ref']
128
- end
129
- return self unless ref.is_a?(String)
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
- # the parent of this node. if this node is the document root (its path is empty), raises
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
- if path.empty?
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
- # we need to preserve the rest of the document, but modify the content at our path.
184
- #
185
- # this is actually a bit tricky. we can't modify the original document, obviously.
186
- # we could do a deep copy, but that's expensive. instead, we make a copy of each array
187
- # or hash in the path above this node. this node's content is modified by the caller, and
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 paths. note that this means two nodes with the same content may not be
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
- {is_node: self.is_a?(JSI::JSON::Node), document: document, path: path}
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 Arraylike
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 Hashlike
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
@@ -11,66 +11,88 @@ module JSI
11
11
  class ReferenceError < Error
12
12
  end
13
13
 
14
- # parse a fragment to an array of reference tokens
14
+ # instantiates a Pointer from any given reference tokens.
15
15
  #
16
- # #/foo/bar
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
- # => ['foo', 'bar']
23
+ # note in the last example that you can conveniently chain the class .[] method
24
+ # with the instance #[] method.
19
25
  #
20
- # #/foo%20bar
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
- # => ['foo bar']
23
- def self.parse_fragment(fragment)
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
- parse_pointer(match.post_match)
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 to an array of reference tokens
34
- #
35
- # /foo
58
+ # parse a pointer string and instantiate as a JSI::JSON::Pointer
36
59
  #
37
- # => ['foo']
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
- # => ['foo~bar', 'baz/qux']
42
- def self.parse_pointer(pointer_string)
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 representation.
56
- #
57
- # type may be one of:
86
+ # initializes a JSI::JSON::Pointer from the given reference_tokens.
58
87
  #
59
- # - :fragment - the representation is a fragment containing a pointer (starting with #)
60
- # - :pointer - the representation is a pointer (starting with /)
61
- # - :reference_tokens - the representation is an array of tokens referencing a path in a document
62
- def initialize(type, representation)
63
- @type = type
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
- "#<#{self.class.inspect} #{@type} = #{representation_s}>"
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 == :fragment
127
- fragment
128
- elsif @type == :pointer
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