lib/jsi/ptr.rb in jsi-0.7.0 vs lib/jsi/ptr.rb in jsi-0.8.0
- old
+ new
@@ -14,16 +14,21 @@
# raised when a pointer refers to a path in a document that could not be resolved
class ResolutionError < Error
end
+ POS_INT_RE = /\A[1-9]\d*\z/
+ private_constant :POS_INT_RE
+
# instantiates a pointer or returns the given pointer
# @param ary_ptr [#to_ary, JSI::Ptr] an array of tokens, or a pointer
# @return [JSI::Ptr]
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
@@ -40,11 +45,11 @@
# class .[] method with the instance #[] method.
#
# @param tokens any number of tokens
# @return [JSI::Ptr]
def self.[](*tokens)
- new(tokens)
+ tokens.empty? ? EMPTY : new(tokens.freeze)
end
# parse a URI-escaped fragment and instantiate as a JSI::Ptr
#
# JSI::Ptr.from_fragment('/foo/bar')
@@ -53,10 +58,16 @@
# 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"]
+ #
# @param fragment [String] a fragment containing a pointer
# @return [JSI::Ptr]
# @raise [JSI::Ptr::PointerSyntaxError] when the fragment does not contain a pointer with
# valid pointer syntax
def self.from_fragment(fragment)
@@ -73,17 +84,21 @@
#
# @param pointer_string [String] a pointer string
# @return [JSI::Ptr]
# @raise [JSI::Ptr::PointerSyntaxError] when the pointer_string does not have valid pointer syntax
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)
+ 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
@@ -92,11 +107,11 @@
# @param tokens [Array<Object>]
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
+ @tokens = Util.deep_to_frozen(tokens.to_ary, not_implemented: proc { |o| o })
end
attr_reader :tokens
# takes a root json document and evaluates this pointer through the document, returning the value
@@ -115,23 +130,23 @@
end
# the pointer string representation of this pointer
# @return [String]
def pointer
- tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('')
+ tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('').freeze
end
# the fragment string representation of this pointer
# @return [String]
def fragment
- Addressable::URI.escape(pointer)
+ Addressable::URI.escape(pointer).freeze
end
# a URI consisting of a fragment containing this pointer's fragment string representation
# @return [Addressable::URI]
def uri
- Addressable::URI.new(fragment: fragment)
+ Addressable::URI.new(fragment: fragment).freeze
end
# whether this pointer is empty, i.e. it has no tokens
# @return [Boolean]
def empty?
@@ -147,33 +162,37 @@
# @raise [JSI::Ptr::Error] if this pointer has no parent (points to the root)
def parent
if root?
raise(Ptr::Error, "cannot access parent of root pointer: #{pretty_inspect.chomp}")
end
- Ptr.new(tokens[0...-1])
+ tokens.size == 1 ? EMPTY : Ptr.new(tokens[0...-1].freeze)
end
- # 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.
+ # whether this pointer is an ancestor of `other_ptr`, a descendent pointer.
+ # `ancestor_of?` is inclusive; a pointer is an ancestor of itself.
+ #
# @return [Boolean]
- def contains?(other_ptr)
+ def ancestor_of?(other_ptr)
tokens == other_ptr.tokens[0...tokens.size]
end
+ # @deprecated
+ def contains?(other_ptr)
+ ancestor_of?(other_ptr)
+ end
+
# part of this pointer relative to the given ancestor_ptr
# @return [JSI::Ptr]
# @raise [JSI::Ptr::Error] if the given ancestor_ptr is not an ancestor of this pointer
def relative_to(ancestor_ptr)
- unless ancestor_ptr.contains?(self)
+ 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
- Ptr.new(tokens[ancestor_ptr.tokens.size..-1])
+ ancestor_ptr.tokens.size == tokens.size ? EMPTY : Ptr.new(tokens[ancestor_ptr.tokens.size..-1].freeze)
end
- # @deprecated after v0.6
- alias_method :ptr_relative_to, :relative_to
-
# a pointer with the tokens of this one plus the given `ptr`'s.
# @param ptr [JSI::Ptr, #to_ary]
# @return [JSI::Ptr]
def +(ptr)
if ptr.is_a?(Ptr)
@@ -181,30 +200,30 @@
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)
+ ptr_tokens.empty? ? self : Ptr.new((tokens + ptr_tokens).freeze)
end
# a pointer consisting of the first `n` of our tokens
# @param n [Integer]
# @return [JSI::Ptr]
# @raise [ArgumentError] if n is not between 0 and the size of our tokens
def take(n)
- unless (0..tokens.size).include?(n)
+ unless n.is_a?(Integer) && n >= 0 && n <= tokens.size
raise(ArgumentError, "n not in range (0..#{tokens.size}): #{n.inspect}")
end
- Ptr.new(tokens.take(n))
+ n == tokens.size ? self : Ptr.new(tokens.take(n).freeze)
end
# appends the given token to this pointer's tokens and returns the result
#
# @param token [Object]
# @return [JSI::Ptr] pointer to a child node of this pointer with the given token
def [](token)
- Ptr.new(tokens + [token])
+ Ptr.new(tokens.dup.push(token).freeze)
end
# 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
@@ -225,11 +244,11 @@
# 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])
+ 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
@@ -244,36 +263,39 @@
end
# a string representation of this pointer
# @return [String]
def inspect
- "#{self.class.name}[#{tokens.map(&:inspect).join(", ")}]"
+ -"#{self.class.name}[#{tokens.map(&:inspect).join(", ")}]"
end
- alias_method :to_s, :inspect
+ def to_s
+ inspect
+ end
- # pointers are equal if the tokens are equal
+ # see {Util::Private::FingerprintHash}
+ # @api private
def jsi_fingerprint
- {class: Ptr, tokens: tokens}
+ {class: Ptr, tokens: tokens}.freeze
end
- include Util::FingerprintHash
+ include Util::FingerprintHash::Immutable
+ EMPTY = new(Util::EMPTY_ARY)
+
private
def node_subscript_token_child(value, token, *a, **kw)
if value.respond_to?(:to_ary)
- if token.is_a?(String) && token =~ /\A\d|[1-9]\d+\z/
+ if token.is_a?(String) && (token == '0' || token =~ POS_INT_RE)
token = token.to_i
elsif token == '-'
# per rfc6901, - refers "to the (nonexistent) member after the last array element" and is
# expected to raise an error condition.
raise(ResolutionError, "Invalid resolution: #{token.inspect} refers to a nonexistent element in array #{value.inspect}")
end
- unless token.is_a?(Integer)
- raise(ResolutionError, "Invalid resolution: #{token.inspect} is not an integer and cannot be resolved in array #{value.inspect}")
- end
- unless (0...(value.respond_to?(:size) ? value : value.to_ary).size).include?(token)
- raise(ResolutionError, "Invalid resolution: #{token.inspect} is not a valid index of #{value.inspect}")
+ size = (value.respond_to?(:size) ? value : value.to_ary).size
+ unless token.is_a?(Integer) && token >= 0 && token < size
+ raise(ResolutionError, "Invalid resolution: #{token.inspect} is not a valid array index of #{value.inspect}")
end
ary = (value.respond_to?(:[]) ? value : value.to_ary)
if kw.empty?
# TODO remove eventually (keyword argument compatibility)