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.
@@ -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