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


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

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.



115
116
117
# File 'lib/jsi/ptr.rb', line 115

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:



49
50
51
# File 'lib/jsi/ptr.rb', line 49

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
32
33
# File 'lib/jsi/ptr.rb', line 25

def self.ary_ptr(ary_ptr)
  if ary_ptr.is_a?(Ptr)
    ary_ptr
  elsif ary_ptr == Util::EMPTY_ARY
    EMPTY
  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:



73
74
75
# File 'lib/jsi/ptr.rb', line 73

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:



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

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:



197
198
199
200
201
202
203
204
205
206
# File 'lib/jsi/ptr.rb', line 197

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



223
224
225
# File 'lib/jsi/ptr.rb', line 223

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

#ancestor_of?(other_ptr) ⇒ Boolean

whether this pointer is an ancestor of other_ptr, a descendent pointer. ancestor_of? is inclusive; a pointer is an ancestor of itself.

Returns:

  • (Boolean)


174
175
176
# File 'lib/jsi/ptr.rb', line 174

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

#contains?(other_ptr) ⇒ Boolean

Deprecated.

Returns:

  • (Boolean)


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

def contains?(other_ptr)
  ancestor_of?(other_ptr)
end

#empty?Boolean Also known as: root?

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

Returns:

  • (Boolean)


152
153
154
# File 'lib/jsi/ptr.rb', line 152

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:



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

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)


140
141
142
# File 'lib/jsi/ptr.rb', line 140

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

#inspectString

a string representation of this pointer

Returns:

  • (String)


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

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



277
278
279
# File 'lib/jsi/ptr.rb', line 277

def jsi_fingerprint
  {class: Ptr, tokens: tokens}.freeze
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



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/jsi/ptr.rb', line 238

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 = tokens.size == 1 ? EMPTY : 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:



163
164
165
166
167
168
# File 'lib/jsi/ptr.rb', line 163

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)


134
135
136
# File 'lib/jsi/ptr.rb', line 134

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



186
187
188
189
190
191
192
# File 'lib/jsi/ptr.rb', line 186

def relative_to(ancestor_ptr)
  return self if ancestor_ptr.empty?
  unless ancestor_ptr.ancestor_of?(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



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

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

#to_sObject



271
272
273
# File 'lib/jsi/ptr.rb', line 271

def to_s
  inspect
end

#uriAddressable::URI

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

Returns:

  • (Addressable::URI)


146
147
148
# File 'lib/jsi/ptr.rb', line 146

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