Class: JSI::Ptr

Inherits:
Object
  • Object
show all
Includes:
Util::FingerprintHash
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

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>)


93
94
95
96
97
98
# File 'lib/jsi/ptr.rb', line 93

def initialize(tokens)
  unless tokens.respond_to?(:to_ary)
    raise(TypeError, "tokens must be an array. got: #{tokens.inspect}")
  end
  @tokens = tokens.to_ary.map(&:freeze).freeze
end

Instance Attribute Details

#tokensObject (readonly)

Returns the value of attribute tokens.



100
101
102
# File 'lib/jsi/ptr.rb', line 100

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:



44
45
46
# File 'lib/jsi/ptr.rb', line 44

def self.[](*tokens)
  new(tokens)
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:



22
23
24
25
26
27
28
# File 'lib/jsi/ptr.rb', line 22

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"]

Parameters:

  • fragment (String)

    a fragment containing a pointer

Returns:

Raises:



62
63
64
# File 'lib/jsi/ptr.rb', line 62

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:



77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/jsi/ptr.rb', line 77

def self.from_pointer(pointer_string)
  tokens = pointer_string.split('/', -1).map! do |piece|
    piece.gsub('~1', '/').gsub('~0', '~')
  end
  if tokens[0] == ''
    new(tokens[1..-1])
  elsif tokens.empty?
    new(tokens)
  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:



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

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.new(tokens + ptr_tokens)
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



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

def [](token)
  Ptr.new(tokens + [token])
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)


158
159
160
# File 'lib/jsi/ptr.rb', line 158

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)


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

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:



109
110
111
112
113
114
115
# File 'lib/jsi/ptr.rb', line 109

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)


125
126
127
# File 'lib/jsi/ptr.rb', line 125

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

#inspectString Also known as: to_s

a string representation of this pointer

Returns:

  • (String)


248
249
250
# File 'lib/jsi/ptr.rb', line 248

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

#jsi_fingerprintObject

pointers are equal if the tokens are equal



255
256
257
# File 'lib/jsi/ptr.rb', line 255

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



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/jsi/ptr.rb', line 219

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])
    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:



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

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

#pointerString

the pointer string representation of this pointer

Returns:

  • (String)


119
120
121
# File 'lib/jsi/ptr.rb', line 119

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

#relative_to(ancestor_ptr) ⇒ JSI::Ptr Also known as: ptr_relative_to

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



165
166
167
168
169
170
# File 'lib/jsi/ptr.rb', line 165

def relative_to(ancestor_ptr)
  unless ancestor_ptr.contains?(self)
    raise(Error, "ancestor_ptr #{ancestor_ptr.inspect} is not ancestor of #{inspect}")
  end
  Ptr.new(tokens[ancestor_ptr.tokens.size..-1])
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



193
194
195
196
197
198
# File 'lib/jsi/ptr.rb', line 193

def take(n)
  unless (0..tokens.size).include?(n)
    raise(ArgumentError, "n not in range (0..#{tokens.size}): #{n.inspect}")
  end
  Ptr.new(tokens.take(n))
end

#uriAddressable::URI

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

Returns:

  • (Addressable::URI)


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

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