Class: JSI::Ptr

Inherits:
Object
  • Object
show all
Includes:
Util::FingerprintHash::Immutable
Defined in:
lib/jsi/ptr.rb

Overview

a representation to work with JSON Pointer, as described by RFC 6901 https://tools.ietf.org/html/rfc6901

a pointer is a sequence of tokens pointing to a node in a document.

Defined Under Namespace

Classes: Error, PointerSyntaxError, ResolutionError

Constant Summary collapse

EMPTY =
new(Util::EMPTY_ARY)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tokens) ⇒ Ptr

initializes a JSI::Ptr from the given tokens.

Parameters:

  • tokens (Array<Object>)


106
107
108
109
110
111
# File 'lib/jsi/ptr.rb', line 106

def initialize(tokens)
  unless tokens.respond_to?(:to_ary)
    raise(TypeError, "tokens must be an array. got: #{tokens.inspect}")
  end
  @tokens = Util.deep_to_frozen(tokens.to_ary, not_implemented: proc { |o| o })
end

Instance Attribute Details

#tokensObject (readonly)

Returns the value of attribute tokens.



113
114
115
# File 'lib/jsi/ptr.rb', line 113

def tokens
  @tokens
end

Class Method Details

.[](*tokens) ⇒ JSI::Ptr

instantiates a pointer from the given tokens.

JSI::Ptr[]

instantiates a root pointer.

JSI::Ptr['a', 'b']
JSI::Ptr['a']['b']

are both ways to instantiate a pointer with tokens ['a', 'b']. the latter example chains the class .[] method with the instance #[] method.

Parameters:

  • tokens

    any number of tokens

Returns:



47
48
49
# File 'lib/jsi/ptr.rb', line 47

def self.[](*tokens)
  tokens.empty? ? EMPTY : new(tokens.freeze)
end

.ary_ptr(ary_ptr) ⇒ JSI::Ptr

instantiates a pointer or returns the given pointer

Parameters:

  • ary_ptr (#to_ary, JSI::Ptr)

    an array of tokens, or a pointer

Returns:



25
26
27
28
29
30
31
# File 'lib/jsi/ptr.rb', line 25

def self.ary_ptr(ary_ptr)
  if ary_ptr.is_a?(Ptr)
    ary_ptr
  else
    new(ary_ptr)
  end
end

.from_fragment(fragment) ⇒ JSI::Ptr

parse a URI-escaped fragment and instantiate as a JSI::Ptr

JSI::Ptr.from_fragment('/foo/bar')
=> JSI::Ptr["foo", "bar"]

with URI escaping:

JSI::Ptr.from_fragment('/foo%20bar')
=> JSI::Ptr["foo bar"]

Note: A fragment does not include a leading '#'. The string "#/foo" is a URI containing the fragment "/foo", which should be parsed by Addressable::URI before passing to this method, e.g.:

JSI::Ptr.from_fragment(Addressable::URI.parse("#/foo").fragment)
=> JSI::Ptr["foo"]

Parameters:

  • fragment (String)

    a fragment containing a pointer

Returns:

Raises:



71
72
73
# File 'lib/jsi/ptr.rb', line 71

def self.from_fragment(fragment)
  from_pointer(Addressable::URI.unescape(fragment))
end

.from_pointer(pointer_string) ⇒ JSI::Ptr

parse a pointer string and instantiate as a JSI::Ptr

JSI::Ptr.from_pointer('/foo')
=> JSI::Ptr["foo"]

JSI::Ptr.from_pointer('/foo~0bar/baz~1qux')
=> JSI::Ptr["foo~bar", "baz/qux"]

Parameters:

  • pointer_string (String)

    a pointer string

Returns:

Raises:



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/jsi/ptr.rb', line 86

def self.from_pointer(pointer_string)
  pointer_string = pointer_string.to_str
  if pointer_string[0] == ?/
    tokens = pointer_string.split('/', -1).map! do |piece|
      piece.gsub!('~1', '/')
      piece.gsub!('~0', '~')
      piece.freeze
    end
    tokens.shift
    new(tokens.freeze)
  elsif pointer_string.empty?
    EMPTY
  else
    raise(PointerSyntaxError, "Invalid pointer syntax in #{pointer_string.inspect}: pointer must begin with /")
  end
end

Instance Method Details

#+(ptr) ⇒ JSI::Ptr

a pointer with the tokens of this one plus the given ptr's.

Parameters:

Returns:



188
189
190
191
192
193
194
195
196
197
# File 'lib/jsi/ptr.rb', line 188

def +(ptr)
  if ptr.is_a?(Ptr)
    ptr_tokens = ptr.tokens
  elsif ptr.respond_to?(:to_ary)
    ptr_tokens = ptr
  else
    raise(TypeError, "ptr must be a #{Ptr} or Array of tokens; got: #{ptr.inspect}")
  end
  ptr_tokens.empty? ? self : Ptr.new((tokens + ptr_tokens).freeze)
end

#[](token) ⇒ JSI::Ptr

appends the given token to this pointer's tokens and returns the result

Parameters:

  • token (Object)

Returns:

  • (JSI::Ptr)

    pointer to a child node of this pointer with the given token



214
215
216
# File 'lib/jsi/ptr.rb', line 214

def [](token)
  Ptr.new(tokens.dup.push(token).freeze)
end

#contains?(other_ptr) ⇒ Boolean

whether this pointer contains the other_ptr - that is, whether this pointer is an ancestor of other_ptr, a descendent pointer. contains? is inclusive; a pointer does contain itself.

Returns:

  • (Boolean)


171
172
173
# File 'lib/jsi/ptr.rb', line 171

def contains?(other_ptr)
  tokens == other_ptr.tokens[0...tokens.size]
end

#empty?Boolean Also known as: root?

whether this pointer is empty, i.e. it has no tokens

Returns:

  • (Boolean)


150
151
152
# File 'lib/jsi/ptr.rb', line 150

def empty?
  tokens.empty?
end

#evaluate(document, *a, **kw) ⇒ Object

takes a root json document and evaluates this pointer through the document, returning the value pointed to by this pointer.

Parameters:

  • document (#to_ary, #to_hash)

    the document against which we will evaluate this pointer

  • a

    arguments are passed to each invocation of #[]

Returns:

  • (Object)

    the content of the document pointed to by this pointer

Raises:



122
123
124
125
126
127
128
# File 'lib/jsi/ptr.rb', line 122

def evaluate(document, *a, **kw)
  res = tokens.inject(document) do |value, token|
    _, child = node_subscript_token_child(value, token, *a, **kw)
    child
  end
  res
end

#fragmentString

the fragment string representation of this pointer

Returns:

  • (String)


138
139
140
# File 'lib/jsi/ptr.rb', line 138

def fragment
  Addressable::URI.escape(pointer).freeze
end

#inspectString Also known as: to_s

a string representation of this pointer

Returns:

  • (String)


258
259
260
# File 'lib/jsi/ptr.rb', line 258

def inspect
  -"#{self.class.name}[#{tokens.map(&:inspect).join(", ")}]"
end

#jsi_fingerprintObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

see Util::Private::FingerprintHash



266
267
268
# File 'lib/jsi/ptr.rb', line 266

def jsi_fingerprint
  {class: Ptr, tokens: tokens}
end

#modified_document_copy(document) {|Object| ... } ⇒ Object

takes a document and a block. the block is yielded the content of the given document at this pointer's location. the block must result a modified copy of that content (and MUST NOT modify the object it is given). this modified copy of that content is incorporated into a modified copy of the given document, which is then returned. the structure and contents of the document outside the path pointed to by this pointer is not modified.

Parameters:

  • document (Object)

    the document to apply this pointer to

Yields:

  • (Object)

    the content this pointer applies to in the given document the block must result in the new content which will be placed in the modified document copy.

Returns:

  • (Object)

    a copy of the given document, with the content this pointer applies to replaced by the result of the block



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/jsi/ptr.rb', line 229

def modified_document_copy(document, &block)
  # we need to preserve the rest of the document, but modify the content at our path.
  #
  # this is actually a bit tricky. we can't modify the original document, obviously.
  # we could do a deep copy, but that's expensive. instead, we make a copy of each array
  # or hash in the path above the node we point to. this node's content is modified by the
  # caller, and that is recursively merged up to the document root.
  if empty?
    Util.modified_copy(document, &block)
  else
    car = tokens[0]
    cdr = Ptr.new(tokens[1..-1].freeze)
    token, document_child = node_subscript_token_child(document, car)
    modified_document_child = cdr.modified_document_copy(document_child, &block)
    if modified_document_child.object_id == document_child.object_id
      document
    else
      modified_document = document.respond_to?(:[]=) ? document.dup :
        document.respond_to?(:to_hash) ? document.to_hash.dup :
        document.respond_to?(:to_ary) ? document.to_ary.dup :
        raise(Bug) # not possible; node_subscript_token_child would have raised
      modified_document[token] = modified_document_child
      modified_document
    end
  end
end

#parentJSI::Ptr

pointer to the parent of where this pointer points

Returns:

Raises:



161
162
163
164
165
166
# File 'lib/jsi/ptr.rb', line 161

def parent
  if root?
    raise(Ptr::Error, "cannot access parent of root pointer: #{pretty_inspect.chomp}")
  end
  tokens.size == 1 ? EMPTY : Ptr.new(tokens[0...-1].freeze)
end

#pointerString

the pointer string representation of this pointer

Returns:

  • (String)


132
133
134
# File 'lib/jsi/ptr.rb', line 132

def pointer
  tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('').freeze
end

#relative_to(ancestor_ptr) ⇒ JSI::Ptr

part of this pointer relative to the given ancestor_ptr

Returns:

Raises:

  • (JSI::Ptr::Error)

    if the given ancestor_ptr is not an ancestor of this pointer



178
179
180
181
182
183
# File 'lib/jsi/ptr.rb', line 178

def relative_to(ancestor_ptr)
  unless ancestor_ptr.contains?(self)
    raise(Error, "ancestor_ptr #{ancestor_ptr.inspect} is not ancestor of #{inspect}")
  end
  ancestor_ptr.tokens.size == tokens.size ? EMPTY : Ptr.new(tokens[ancestor_ptr.tokens.size..-1].freeze)
end

#take(n) ⇒ JSI::Ptr

a pointer consisting of the first n of our tokens

Parameters:

  • n (Integer)

Returns:

Raises:

  • (ArgumentError)

    if n is not between 0 and the size of our tokens



203
204
205
206
207
208
# File 'lib/jsi/ptr.rb', line 203

def take(n)
  unless n.is_a?(Integer) && n >= 0 && n <= tokens.size
    raise(ArgumentError, "n not in range (0..#{tokens.size}): #{n.inspect}")
  end
  n == tokens.size ? self : Ptr.new(tokens.take(n).freeze)
end

#uriAddressable::URI

a URI consisting of a fragment containing this pointer's fragment string representation

Returns:

  • (Addressable::URI)


144
145
146
# File 'lib/jsi/ptr.rb', line 144

def uri
  Addressable::URI.new(fragment: fragment).freeze
end