module ObjectPatch::Pointer

This module contains the code to convert between an JSON pointer path representation and the keys required to traverse an array. It can make use of an a path and evaluate it against a provided (potentially deeply nested) array or hash.

This is mostly compliant with RFC6901, however, a few small exceptions have been made, though they shouldn’t break compatibility with pure implementations.

Public Class Methods

encode(ary_path) click to toggle source

Given an array of keys this will provide a properly escaped JSONPointer path.

@param [Array<String,Fixnum>] ary_path @return [String]

# File lib/object_patch/pointer.rb, line 47
def encode(ary_path)
  ary_path = Array(ary_path).map { |p| p.is_a?(String) ? escape(p) : p }
  "/" << ary_path.join("/")
end
escape(str) click to toggle source

Escapes reserved characters as defined by RFC6901. This is intended to escape individual segments of the pointer and thus should not be run on an already generated path.

@see [Pointer#unescape] @param [String] str @return [String]

# File lib/object_patch/pointer.rb, line 59
def escape(str)
  str.gsub(/~|\//, { '~' => '~0', '/' => '~1' })
end
eval(path, obj) click to toggle source

Given a parsed path and an object, get the nested value within the object.

@param [Array<String,Fixnum>] path Key path to traverse to get the value. @param [Hash,Array] obj The document to traverse. @return [Object] The value at the provided path.

# File lib/object_patch/pointer.rb, line 19
def eval(path, obj)
  path.inject(obj) do |o, p|
    if o.is_a?(Hash)
      raise MissingTargetException unless o.keys.include?(p)
      o[p]
    elsif o.is_a?(Array)
      # The last element +1 is technically how this is interpretted. This
      # will always trigger the index error so it may not be valuable to
      # set...
      p = o.size if p == "-1"
      # Technically a violation of the RFC to allow reverse access to the
      # array but I'll allow it...
      raise ObjectOperationOnArrayException unless p.to_s.match(/\A-?\d+\Z/)
      raise InvalidIndexError unless p.to_i.abs < o.size
      o[p.to_i]
    else
      # We received a Scalar value from the prior iteration... we can't do
      # anything with this...
      raise TraverseScalarException
    end
  end
end
parse(path) click to toggle source

Convert a JSON pointer into an array of keys that can be used to traverse a parsed JSON document.

@param [String] path @return [Array<String,Fixnum>]

# File lib/object_patch/pointer.rb, line 68
def parse(path)
  # I'm pretty sure this isn't quite valid but it's a holdover from
  # tenderlove's code. Once the operations are refactored I believe this
  # won't be necessary.
  return [""] if path == "/"
  # Strip off the leading slash
  path = path.sub(/^\//, '')
  path.split("/").map { |p| unescape(p) }
end
unescape(str) click to toggle source

Unescapes any reserved characters within a JSON pointer segment.

@see [Pointer#escape] @param [String] str @return [String]

# File lib/object_patch/pointer.rb, line 83
def unescape(str)
  str.gsub(/~[01]/, { '~0' => '~', '~1' => '/' })
end

Private Instance Methods

encode(ary_path) click to toggle source

Given an array of keys this will provide a properly escaped JSONPointer path.

@param [Array<String,Fixnum>] ary_path @return [String]

# File lib/object_patch/pointer.rb, line 47
def encode(ary_path)
  ary_path = Array(ary_path).map { |p| p.is_a?(String) ? escape(p) : p }
  "/" << ary_path.join("/")
end
escape(str) click to toggle source

Escapes reserved characters as defined by RFC6901. This is intended to escape individual segments of the pointer and thus should not be run on an already generated path.

@see [Pointer#unescape] @param [String] str @return [String]

# File lib/object_patch/pointer.rb, line 59
def escape(str)
  str.gsub(/~|\//, { '~' => '~0', '/' => '~1' })
end
eval(path, obj) click to toggle source

Given a parsed path and an object, get the nested value within the object.

@param [Array<String,Fixnum>] path Key path to traverse to get the value. @param [Hash,Array] obj The document to traverse. @return [Object] The value at the provided path.

# File lib/object_patch/pointer.rb, line 19
def eval(path, obj)
  path.inject(obj) do |o, p|
    if o.is_a?(Hash)
      raise MissingTargetException unless o.keys.include?(p)
      o[p]
    elsif o.is_a?(Array)
      # The last element +1 is technically how this is interpretted. This
      # will always trigger the index error so it may not be valuable to
      # set...
      p = o.size if p == "-1"
      # Technically a violation of the RFC to allow reverse access to the
      # array but I'll allow it...
      raise ObjectOperationOnArrayException unless p.to_s.match(/\A-?\d+\Z/)
      raise InvalidIndexError unless p.to_i.abs < o.size
      o[p.to_i]
    else
      # We received a Scalar value from the prior iteration... we can't do
      # anything with this...
      raise TraverseScalarException
    end
  end
end
parse(path) click to toggle source

Convert a JSON pointer into an array of keys that can be used to traverse a parsed JSON document.

@param [String] path @return [Array<String,Fixnum>]

# File lib/object_patch/pointer.rb, line 68
def parse(path)
  # I'm pretty sure this isn't quite valid but it's a holdover from
  # tenderlove's code. Once the operations are refactored I believe this
  # won't be necessary.
  return [""] if path == "/"
  # Strip off the leading slash
  path = path.sub(/^\//, '')
  path.split("/").map { |p| unescape(p) }
end
unescape(str) click to toggle source

Unescapes any reserved characters within a JSON pointer segment.

@see [Pointer#escape] @param [String] str @return [String]

# File lib/object_patch/pointer.rb, line 83
def unescape(str)
  str.gsub(/~[01]/, { '~0' => '~', '~1' => '/' })
end