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)