lib/addressable/template.rb



# encoding:utf-8
#--
# Copyright (C) 2006-2011 Bob Aman
#
#    Licensed under the Apache License, Version 2.0 (the "License");
#    you may not use this file except in compliance with the License.
#    You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS,
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#    See the License for the specific language governing permissions and
#    limitations under the License.
#++


require "addressable/version"
require "addressable/uri"

module Addressable
  ##
  # This is an implementation of a URI template based on
  # <a href="http://tinyurl.com/uritemplatedraft03">URI Template draft 03</a>.
  class Template
    # Constants used throughout the template code.
    anything =
      Addressable::URI::CharacterClasses::RESERVED +
      Addressable::URI::CharacterClasses::UNRESERVED
    OPERATOR_EXPANSION =
      /\{-([a-zA-Z]+)\|([#{anything}]+)\|([#{anything}]+)\}/
    VARIABLE_EXPANSION = /\{([#{anything}]+?)(?:=([#{anything}]+))?\}/

    ##
    # Raised if an invalid template value is supplied.
    class InvalidTemplateValueError < StandardError
    end

    ##
    # Raised if an invalid template operator is used in a pattern.
    class InvalidTemplateOperatorError < StandardError
    end

    ##
    # Raised if an invalid template operator is used in a pattern.
    class TemplateOperatorAbortedError < StandardError
    end

    ##
    # This class represents the data that is extracted when a Template
    # is matched against a URI.
    class MatchData
      ##
      # Creates a new MatchData object.
      # MatchData objects should never be instantiated directly.
      #
      # @param [Addressable::URI] uri
      #   The URI that the template was matched against.
      def initialize(uri, template, mapping)
        @uri = uri.dup.freeze
        @template = template
        @mapping = mapping.dup.freeze
      end

      ##
      # @return [Addressable::URI]
      #   The URI that the Template was matched against.
      attr_reader :uri

      ##
      # @return [Addressable::Template]
      #   The Template used for the match.
      attr_reader :template

      ##
      # @return [Hash]
      #   The mapping that resulted from the match.
      #   Note that this mapping does not include keys or values for
      #   variables that appear in the Template, but are not present
      #   in the URI.
      attr_reader :mapping

      ##
      # @return [Array]
      #   The list of variables that were present in the Template.
      #   Note that this list will include variables which do not appear
      #   in the mapping because they were not present in URI.
      def variables
        self.template.variables
      end
      alias_method :keys, :variables

      ##
      # @return [Array]
      #   The list of values that were captured by the Template.
      #   Note that this list will include nils for any variables which
      #   were in the Template, but did not appear in the URI.
      def values
        @values ||= self.variables.inject([]) do |accu, key|
          accu << self.mapping[key]
          accu
        end
      end
      alias_method :captures, :values

      ##
      # Returns a <tt>String</tt> representation of the MatchData's state.
      #
      # @return [String] The MatchData's state, as a <tt>String</tt>.
      def inspect
        sprintf("#<%s:%#0x RESULT:%s>",
          self.class.to_s, self.object_id, self.mapping.inspect)
      end
    end

    ##
    # Creates a new <tt>Addressable::Template</tt> object.
    #
    # @param [#to_str] pattern The URI Template pattern.
    #
    # @return [Addressable::Template] The initialized Template object.
    def initialize(pattern)
      if !pattern.respond_to?(:to_str)
        raise TypeError, "Can't convert #{pattern.class} into String."
      end
      @pattern = pattern.to_str.freeze
    end

    ##
    # @return [String] The Template object's pattern.
    attr_reader :pattern

    ##
    # Returns a <tt>String</tt> representation of the Template object's state.
    #
    # @return [String] The Template object's state, as a <tt>String</tt>.
    def inspect
      sprintf("#<%s:%#0x PATTERN:%s>",
        self.class.to_s, self.object_id, self.pattern)
    end

    ##
    # Extracts a mapping from the URI using a URI Template pattern.
    #
    # @param [Addressable::URI, #to_str] uri
    #   The URI to extract from.
    #
    # @param [#restore, #match] processor
    #   A template processor object may optionally be supplied.
    #   
    #   The object should respond to either the <tt>restore</tt> or
    #   <tt>match</tt> messages or both. The <tt>restore</tt> method should
    #   take two parameters: `[String] name` and `[String] value`.
    #   The <tt>restore</tt> method should reverse any transformations that
    #   have been performed on the value to ensure a valid URI.
    #   The <tt>match</tt> method should take a single
    #   parameter: `[String] name`.  The <tt>match</tt> method should return
    #   a <tt>String</tt> containing a regular expression capture group for
    #   matching on that particular variable. The default value is `".*?"`.
    #   The <tt>match</tt> method has no effect on multivariate operator
    #   expansions.
    #
    # @return [Hash, NilClass]
    #   The <tt>Hash</tt> mapping that was extracted from the URI, or
    #   <tt>nil</tt> if the URI didn't match the template.
    #
    # @example
    #   class ExampleProcessor
    #     def self.restore(name, value)
    #       return value.gsub(/\+/, " ") if name == "query"
    #       return value
    #     end
    #
    #     def self.match(name)
    #       return ".*?" if name == "first"
    #       return ".*"
    #     end
    #   end
    #
    #   uri = Addressable::URI.parse(
    #     "http://example.com/search/an+example+search+query/"
    #   )
    #   Addressable::Template.new(
    #     "http://example.com/search/{query}/"
    #   ).extract(uri, ExampleProcessor)
    #   #=> {"query" => "an example search query"}
    #
    #   uri = Addressable::URI.parse("http://example.com/a/b/c/")
    #   Addressable::Template.new(
    #     "http://example.com/{first}/{second}/"
    #   ).extract(uri, ExampleProcessor)
    #   #=> {"first" => "a", "second" => "b/c"}
    #
    #   uri = Addressable::URI.parse("http://example.com/a/b/c/")
    #   Addressable::Template.new(
    #     "http://example.com/{first}/{-list|/|second}/"
    #   ).extract(uri)
    #   #=> {"first" => "a", "second" => ["b", "c"]}
    def extract(uri, processor=nil)
      match_data = self.match(uri, processor)
      return (match_data ? match_data.mapping : nil)
    end

    ##
    # Extracts match data from the URI using a URI Template pattern.
    #
    # @param [Addressable::URI, #to_str] uri
    #   The URI to extract from.
    #
    # @param [#restore, #match] processor
    #   A template processor object may optionally be supplied.
    #   
    #   The object should respond to either the <tt>restore</tt> or
    #   <tt>match</tt> messages or both. The <tt>restore</tt> method should
    #   take two parameters: `[String] name` and `[String] value`.
    #   The <tt>restore</tt> method should reverse any transformations that
    #   have been performed on the value to ensure a valid URI.
    #   The <tt>match</tt> method should take a single
    #   parameter: `[String] name`. The <tt>match</tt> method should return
    #   a <tt>String</tt> containing a regular expression capture group for
    #   matching on that particular variable. The default value is `".*?"`.
    #   The <tt>match</tt> method has no effect on multivariate operator
    #   expansions.
    #
    # @return [Hash, NilClass]
    #   The <tt>Hash</tt> mapping that was extracted from the URI, or
    #   <tt>nil</tt> if the URI didn't match the template.
    #
    # @example
    #   class ExampleProcessor
    #     def self.restore(name, value)
    #       return value.gsub(/\+/, " ") if name == "query"
    #       return value
    #     end
    #
    #     def self.match(name)
    #       return ".*?" if name == "first"
    #       return ".*"
    #     end
    #   end
    #
    #   uri = Addressable::URI.parse(
    #     "http://example.com/search/an+example+search+query/"
    #   )
    #   match = Addressable::Template.new(
    #     "http://example.com/search/{query}/"
    #   ).match(uri, ExampleProcessor)
    #   match.variables
    #   #=> ["query"]
    #   match.captures
    #   #=> ["an example search query"]
    #
    #   uri = Addressable::URI.parse("http://example.com/a/b/c/")
    #   match = Addressable::Template.new(
    #     "http://example.com/{first}/{second}/"
    #   ).match(uri, ExampleProcessor)
    #   match.variables
    #   #=> ["first", "second"]
    #   match.captures
    #   #=> ["a", "b/c"]
    #
    #   uri = Addressable::URI.parse("http://example.com/a/b/c/")
    #   match = Addressable::Template.new(
    #     "http://example.com/{first}/{-list|/|second}/"
    #   ).match(uri)
    #   match.variables
    #   #=> ["first", "second"]
    #   match.captures
    #   #=> ["a", ["b", "c"]]
    def match(uri, processor=nil)
      uri = Addressable::URI.parse(uri)
      mapping = {}

      # First, we need to process the pattern, and extract the values.
      expansions, expansion_regexp =
        parse_template_pattern(pattern, processor)
      unparsed_values = uri.to_str.scan(expansion_regexp).flatten

      if uri.to_str == pattern
        return Addressable::Template::MatchData.new(uri, self, mapping)
      elsif expansions.size > 0 && expansions.size == unparsed_values.size
        expansions.each_with_index do |expansion, index|
          unparsed_value = unparsed_values[index]
          if expansion =~ OPERATOR_EXPANSION
            operator, argument, variables =
              parse_template_expansion(expansion)
            extract_method = "extract_#{operator}_operator"
            if ([extract_method, extract_method.to_sym] &
                private_methods).empty?
              raise InvalidTemplateOperatorError,
                "Invalid template operator: #{operator}"
            else
              begin
                send(
                  extract_method.to_sym, unparsed_value, processor,
                  argument, variables, mapping
                )
              rescue TemplateOperatorAbortedError
                return nil
              end
            end
          else
            name = expansion[VARIABLE_EXPANSION, 1]
            value = unparsed_value
            if processor != nil && processor.respond_to?(:restore)
              value = processor.restore(name, value)
            end
            if mapping[name] == nil || mapping[name] == value
              mapping[name] = value
            else
              return nil
            end
          end
        end
        return Addressable::Template::MatchData.new(uri, self, mapping)
      else
        return nil
      end
    end

    ##
    # Expands a URI template into another URI template.
    #
    # @param [Hash] mapping The mapping that corresponds to the pattern.
    # @param [#validate, #transform] processor
    #   An optional processor object may be supplied. 
    #
    # The object should respond to either the <tt>validate</tt> or
    # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
    # <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
    # <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
    # or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
    # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
    # exception will be raised if the value is invalid. The <tt>transform</tt>
    # method should return the transformed variable value as a <tt>String</tt>.
    # If a <tt>transform</tt> method is used, the value will not be percent
    # encoded automatically. Unicode normalization will be performed both
    # before and after sending the value to the transform method.
    #
    # @return [Addressable::Template] The partially expanded URI template.
    #
    # @example
    #   Addressable::Template.new(
    #     "http://example.com/{one}/{two}/"
    #   ).partial_expand({"one" => "1"}).pattern
    #   #=> "http://example.com/1/{two}/"
    #
    #   Addressable::Template.new(
    #     "http://example.com/search/{-list|+|query}/"
    #   ).partial_expand(
    #     {"query" => "an example search query".split(" ")}
    #   ).pattern
    #   #=> "http://example.com/search/an+example+search+query/"
    #
    #   Addressable::Template.new(
    #     "http://example.com/{-join|&|one,two}/"
    #   ).partial_expand({"one" => "1"}).pattern
    #   #=> "http://example.com/?one=1{-prefix|&two=|two}"
    #
    #   Addressable::Template.new(
    #     "http://example.com/{-join|&|one,two,three}/"
    #   ).partial_expand({"one" => "1", "three" => 3}).pattern
    #   #=> "http://example.com/?one=1{-prefix|&two=|two}&three=3"
    def partial_expand(mapping, processor=nil)
      result = self.pattern.dup
      transformed_mapping = transform_mapping(mapping, processor)
      result.gsub!(
        /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
      ) do |capture|
        if capture =~ OPERATOR_EXPANSION
          operator, argument, variables, default_mapping =
            parse_template_expansion(capture, transformed_mapping)
          expand_method = "expand_#{operator}_operator"
          if ([expand_method, expand_method.to_sym] & private_methods).empty?
            raise InvalidTemplateOperatorError,
              "Invalid template operator: #{operator}"
          else
            send(
              expand_method.to_sym, argument, variables,
              default_mapping, true
            )
          end
        else
          varname, _, vardefault = capture.scan(/^\{(.+?)(=(.*))?\}$/)[0]
          if transformed_mapping[varname]
            transformed_mapping[varname]
          elsif vardefault
            "{#{varname}=#{vardefault}}"
          else
            "{#{varname}}"
          end
        end
      end
      return Addressable::Template.new(result)
    end

    ##
    # Expands a URI template into a full URI.
    #
    # @param [Hash] mapping The mapping that corresponds to the pattern.
    # @param [#validate, #transform] processor
    #   An optional processor object may be supplied.
    #
    # The object should respond to either the <tt>validate</tt> or
    # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
    # <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
    # <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
    # or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
    # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
    # exception will be raised if the value is invalid. The <tt>transform</tt>
    # method should return the transformed variable value as a <tt>String</tt>.
    # If a <tt>transform</tt> method is used, the value will not be percent
    # encoded automatically. Unicode normalization will be performed both
    # before and after sending the value to the transform method.
    #
    # @return [Addressable::URI] The expanded URI template.
    #
    # @example
    #   class ExampleProcessor
    #     def self.validate(name, value)
    #       return !!(value =~ /^[\w ]+$/) if name == "query"
    #       return true
    #     end
    #
    #     def self.transform(name, value)
    #       return value.gsub(/ /, "+") if name == "query"
    #       return value
    #     end
    #   end
    #
    #   Addressable::Template.new(
    #     "http://example.com/search/{query}/"
    #   ).expand(
    #     {"query" => "an example search query"},
    #     ExampleProcessor
    #   ).to_str
    #   #=> "http://example.com/search/an+example+search+query/"
    #
    #   Addressable::Template.new(
    #     "http://example.com/search/{-list|+|query}/"
    #   ).expand(
    #     {"query" => "an example search query".split(" ")}
    #   ).to_str
    #   #=> "http://example.com/search/an+example+search+query/"
    #
    #   Addressable::Template.new(
    #     "http://example.com/search/{query}/"
    #   ).expand(
    #     {"query" => "bogus!"},
    #     ExampleProcessor
    #   ).to_str
    #   #=> Addressable::Template::InvalidTemplateValueError
    def expand(mapping, processor=nil)
      result = self.pattern.dup
      transformed_mapping = transform_mapping(mapping, processor)
      result.gsub!(
        /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
      ) do |capture|
        if capture =~ OPERATOR_EXPANSION
          operator, argument, variables, default_mapping =
            parse_template_expansion(capture, transformed_mapping)
          expand_method = "expand_#{operator}_operator"
          if ([expand_method, expand_method.to_sym] & private_methods).empty?
            raise InvalidTemplateOperatorError,
              "Invalid template operator: #{operator}"
          else
            send(expand_method.to_sym, argument, variables, default_mapping)
          end
        else
          varname, _, vardefault = capture.scan(/^\{(.+?)(=(.*))?\}$/)[0]
          transformed_mapping[varname] || vardefault
        end
      end
      return Addressable::URI.parse(result)
    end

    ##
    # Returns an Array of variables used within the template pattern.
    # The variables are listed in the Array in the order they appear within
    # the pattern.  Multiple occurrences of a variable within a pattern are
    # not represented in this Array.
    #
    # @return [Array] The variables present in the template's pattern.
    def variables
      @variables ||= ordered_variable_defaults.map { |var, val| var }.uniq
    end
    alias_method :keys, :variables

    ##
    # Returns a mapping of variables to their default values specified
    # in the template. Variables without defaults are not returned.
    #
    # @return [Hash] Mapping of template variables to their defaults
    def variable_defaults
      @variable_defaults ||=
        Hash[*ordered_variable_defaults.reject { |k, v| v.nil? }.flatten]
    end

  private
    def ordered_variable_defaults
      @ordered_variable_defaults ||= (begin
        expansions, expansion_regexp = parse_template_pattern(pattern)

        expansions.inject([]) do |result, expansion|
          case expansion
          when OPERATOR_EXPANSION
            _, _, variables, mapping = parse_template_expansion(expansion)
            result.concat variables.map { |var| [var, mapping[var]] }
          when VARIABLE_EXPANSION
            result << [$1, $2]
          end
          result
        end
      end)
    end

    ##
    # Transforms a mapping so that values can be substituted into the
    # template.
    #
    # @param [Hash] mapping The mapping of variables to values.
    # @param [#validate, #transform] processor
    #   An optional processor object may be supplied.
    #
    # The object should respond to either the <tt>validate</tt> or
    # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
    # <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
    # <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
    # or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
    # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt> exception
    # will be raised if the value is invalid. The <tt>transform</tt> method
    # should return the transformed variable value as a <tt>String</tt>. If a
    # <tt>transform</tt> method is used, the value will not be percent encoded
    # automatically. Unicode normalization will be performed both before and
    # after sending the value to the transform method.
    #
    # @return [Hash] The transformed mapping.
    def transform_mapping(mapping, processor=nil)
      return mapping.inject({}) do |accu, pair|
        name, value = pair
        value = value.to_s if Numeric === value || Symbol === value

        unless value.respond_to?(:to_ary) || value.respond_to?(:to_str)
          raise TypeError,
            "Can't convert #{value.class} into String or Array."
        end

        if Symbol === name
          name = name.to_s
        elsif name.respond_to?(:to_str)
          name = name.to_str
        else
          raise TypeError,
            "Can't convert #{name.class} into String."
        end
        value = value.respond_to?(:to_ary) ? value.to_ary : value.to_str

        # Handle unicode normalization
        if value.kind_of?(Array)
          value.map! { |val| Addressable::IDNA.unicode_normalize_kc(val) }
        else
          value = Addressable::IDNA.unicode_normalize_kc(value)
        end

        if processor == nil || !processor.respond_to?(:transform)
          # Handle percent escaping
          if value.kind_of?(Array)
            transformed_value = value.map do |val|
              Addressable::URI.encode_component(
                val, Addressable::URI::CharacterClasses::UNRESERVED)
            end
          else
            transformed_value = Addressable::URI.encode_component(
              value, Addressable::URI::CharacterClasses::UNRESERVED)
          end
        end

        # Process, if we've got a processor
        if processor != nil
          if processor.respond_to?(:validate)
            if !processor.validate(name, value)
              display_value = value.kind_of?(Array) ? value.inspect : value
              raise InvalidTemplateValueError,
                "#{name}=#{display_value} is an invalid template value."
            end
          end
          if processor.respond_to?(:transform)
            transformed_value = processor.transform(name, value)
            if transformed_value.kind_of?(Array)
              transformed_value.map! do |val|
                Addressable::IDNA.unicode_normalize_kc(val)
              end
            else
              transformed_value =
                Addressable::IDNA.unicode_normalize_kc(transformed_value)
            end
          end
        end

        accu[name] = transformed_value
        accu
      end
    end

    ##
    # Expands a URI Template opt operator.
    #
    # @param [String] argument The argument to the operator.
    # @param [Array] variables The variables the operator is working on.
    # @param [Hash] mapping The mapping of variables to values.
    #
    # @return [String] The expanded result.
    def expand_opt_operator(argument, variables, mapping, partial=false)
      variables_present = variables.any? do |variable|
        mapping[variable] != [] &&
        mapping[variable]
      end
      if partial && !variables_present
        "{-opt|#{argument}|#{variables.join(",")}}"
      elsif variables_present
        argument
      else
        ""
      end
    end

    ##
    # Expands a URI Template neg operator.
    #
    # @param [String] argument The argument to the operator.
    # @param [Array] variables The variables the operator is working on.
    # @param [Hash] mapping The mapping of variables to values.
    #
    # @return [String] The expanded result.
    def expand_neg_operator(argument, variables, mapping, partial=false)
      variables_present = variables.any? do |variable|
        mapping[variable] != [] &&
        mapping[variable]
      end
      if partial && !variables_present
        "{-neg|#{argument}|#{variables.join(",")}}"
      elsif variables_present
        ""
      else
        argument
      end
    end

    ##
    # Expands a URI Template prefix operator.
    #
    # @param [String] argument The argument to the operator.
    # @param [Array] variables The variables the operator is working on.
    # @param [Hash] mapping The mapping of variables to values.
    #
    # @return [String] The expanded result.
    def expand_prefix_operator(argument, variables, mapping, partial=false)
      if variables.size != 1
        raise InvalidTemplateOperatorError,
          "Template operator 'prefix' takes exactly one variable."
      end
      value = mapping[variables.first]
      if !partial || value
        if value.kind_of?(Array)
          (value.map { |list_value| argument + list_value }).join("")
        elsif value
          argument + value.to_s
        end
      else
        "{-prefix|#{argument}|#{variables.first}}"
      end
    end

    ##
    # Expands a URI Template suffix operator.
    #
    # @param [String] argument The argument to the operator.
    # @param [Array] variables The variables the operator is working on.
    # @param [Hash] mapping The mapping of variables to values.
    #
    # @return [String] The expanded result.
    def expand_suffix_operator(argument, variables, mapping, partial=false)
      if variables.size != 1
        raise InvalidTemplateOperatorError,
          "Template operator 'suffix' takes exactly one variable."
      end
      value = mapping[variables.first]
      if !partial || value
        if value.kind_of?(Array)
          (value.map { |list_value| list_value + argument }).join("")
        elsif value
          value.to_s + argument
        end
      else
        "{-suffix|#{argument}|#{variables.first}}"
      end
    end

    ##
    # Expands a URI Template join operator.
    #
    # @param [String] argument The argument to the operator.
    # @param [Array] variables The variables the operator is working on.
    # @param [Hash] mapping The mapping of variables to values.
    #
    # @return [String] The expanded result.
    def expand_join_operator(argument, variables, mapping, partial=false)
      if !partial
        variable_values = variables.inject([]) do |accu, variable|
          if !mapping[variable].kind_of?(Array)
            if mapping[variable]
              accu << variable + "=" + (mapping[variable])
            end
          else
            raise InvalidTemplateOperatorError,
              "Template operator 'join' does not accept Array values."
          end
          accu
        end
        variable_values.join(argument)
      else
        buffer = ""
        state = :suffix
        variables.each_with_index do |variable, index|
          if !mapping[variable].kind_of?(Array)
            if mapping[variable]
              if buffer.empty? || buffer[-1..-1] == "}"
                buffer << (variable + "=" + (mapping[variable]))
              elsif state == :suffix
                buffer << argument
                buffer << (variable + "=" + (mapping[variable]))
              else
                buffer << (variable + "=" + (mapping[variable]))
              end
            else
              if !buffer.empty? && (buffer[-1..-1] != "}" || state == :prefix)
                buffer << "{-opt|#{argument}|#{variable}}"
                state = :prefix
              end
              if buffer.empty? && variables.size == 1
                # Evaluates back to itself
                buffer << "{-join|#{argument}|#{variable}}"
              else
                buffer << "{-prefix|#{variable}=|#{variable}}"
              end
              if (index != (variables.size - 1) && state == :suffix)
                buffer << "{-opt|#{argument}|#{variable}}"
              elsif index != (variables.size - 1) &&
                  mapping[variables[index + 1]]
                buffer << argument
                state = :prefix
              end
            end
          else
            raise InvalidTemplateOperatorError,
              "Template operator 'join' does not accept Array values."
          end
        end
        buffer
      end
    end

    ##
    # Expands a URI Template list operator.
    #
    # @param [String] argument The argument to the operator.
    # @param [Array] variables The variables the operator is working on.
    # @param [Hash] mapping The mapping of variables to values.
    #
    # @return [String] The expanded result.
    def expand_list_operator(argument, variables, mapping, partial=false)
      if variables.size != 1
        raise InvalidTemplateOperatorError,
          "Template operator 'list' takes exactly one variable."
      end
      if !partial || mapping[variables.first]
        values = mapping[variables.first]
        if values
          if values.kind_of?(Array)
            values.join(argument)
          else
            raise InvalidTemplateOperatorError,
              "Template operator 'list' only accepts Array values."
          end
        end
      else
        "{-list|#{argument}|#{variables.first}}"
      end
    end

    ##
    # Parses a URI template expansion <tt>String</tt>.
    #
    # @param [String] expansion The operator <tt>String</tt>.
    # @param [Hash] mapping An optional mapping to merge defaults into.
    #
    # @return [Array]
    #   A tuple of the operator, argument, variables, and mapping.
    def parse_template_expansion(capture, mapping={})
      operator, argument, variables = capture[1...-1].split("|", -1)
      operator.gsub!(/^\-/, "")
      variables = variables.split(",", -1)
      mapping = (variables.inject({}) do |accu, var|
        varname, _, vardefault = var.scan(/^(.+?)(=(.*))?$/)[0]
        accu[varname] = vardefault
        accu
      end).merge(mapping)
      variables = variables.map { |var| var.gsub(/=.*$/, "") }
      return operator, argument, variables, mapping
    end

    ##
    # Generates the <tt>Regexp</tt> that parses a template pattern.
    #
    # @param [String] pattern The URI template pattern.
    # @param [#match] processor The template processor to use.
    #
    # @return [Regexp]
    #   A regular expression which may be used to parse a template pattern.
    def parse_template_pattern(pattern, processor=nil)
      # Escape the pattern. The two gsubs restore the escaped curly braces
      # back to their original form. Basically, escape everything that isn't
      # within an expansion.
      escaped_pattern = Regexp.escape(
        pattern
      ).gsub(/\\\{(.*?)\\\}/) do |escaped|
        escaped.gsub(/\\(.)/, "\\1")
      end

      expansions = []

      # Create a regular expression that captures the values of the
      # variables in the URI.
      regexp_string = escaped_pattern.gsub(
        /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
      ) do |expansion|
        expansions << expansion
        if expansion =~ OPERATOR_EXPANSION
          capture_group = "(.*)"
          operator, argument, names, _ =
            parse_template_expansion(expansion)
          if processor != nil && processor.respond_to?(:match)
            # We can only lookup the match values for single variable
            # operator expansions. Besides, ".*" is usually the only
            # reasonable value for multivariate operators anyways.
            if ["prefix", "suffix", "list"].include?(operator)
              capture_group = "(#{processor.match(names.first)})"
            end
          elsif operator == "prefix"
            capture_group = "(#{Regexp.escape(argument)}.*?)"
          elsif operator == "suffix"
            capture_group = "(.*?#{Regexp.escape(argument)})"
          end
          capture_group
        else
          capture_group = "(.*?)"
          if processor != nil && processor.respond_to?(:match)
            name = expansion[/\{([^\}=]+)(=[^\}]+)?\}/, 1]
            capture_group = "(#{processor.match(name)})"
          end
          capture_group
        end
      end

      # Ensure that the regular expression matches the whole URI.
      regexp_string = "^#{regexp_string}$"

      return expansions, Regexp.new(regexp_string)
    end

    ##
    # Extracts a URI Template opt operator.
    #
    # @param [String] value The unparsed value to extract from.
    # @param [#restore] processor The processor object.
    # @param [String] argument The argument to the operator.
    # @param [Array] variables The variables the operator is working on.
    # @param [Hash] mapping The mapping of variables to values.
    #
    # @return [String] The extracted result.
    def extract_opt_operator(
        value, processor, argument, variables, mapping)
      if value != "" && value != argument
        raise TemplateOperatorAbortedError,
          "Value for template operator 'opt' was unexpected."
      end
    end

    ##
    # Extracts a URI Template neg operator.
    #
    # @param [String] value The unparsed value to extract from.
    # @param [#restore] processor The processor object.
    # @param [String] argument The argument to the operator.
    # @param [Array] variables The variables the operator is working on.
    # @param [Hash] mapping The mapping of variables to values.
    #
    # @return [String] The extracted result.
    def extract_neg_operator(
        value, processor, argument, variables, mapping)
      if value != "" && value != argument
        raise TemplateOperatorAbortedError,
          "Value for template operator 'neg' was unexpected."
      end
    end

    ##
    # Extracts a URI Template prefix operator.
    #
    # @param [String] value The unparsed value to extract from.
    # @param [#restore] processor The processor object.
    # @param [String] argument The argument to the operator.
    # @param [Array] variables The variables the operator is working on.
    # @param [Hash] mapping The mapping of variables to values.
    #
    # @return [String] The extracted result.
    def extract_prefix_operator(
        value, processor, argument, variables, mapping)
      if variables.size != 1
        raise InvalidTemplateOperatorError,
          "Template operator 'prefix' takes exactly one variable."
      end
      if value[0...argument.size] != argument
        raise TemplateOperatorAbortedError,
          "Value for template operator 'prefix' missing expected prefix."
      end
      values = value.split(argument, -1)
      values << "" if value[-argument.size..-1] == argument
      values.shift if values[0] == ""
      values.pop if values[-1] == ""

      if processor && processor.respond_to?(:restore)
        values.map! { |value| processor.restore(variables.first, value) }
      end
      values = values.first if values.size == 1
      if mapping[variables.first] == nil || mapping[variables.first] == values
        mapping[variables.first] = values
      else
        raise TemplateOperatorAbortedError,
          "Value mismatch for repeated variable."
      end
    end

    ##
    # Extracts a URI Template suffix operator.
    #
    # @param [String] value The unparsed value to extract from.
    # @param [#restore] processor The processor object.
    # @param [String] argument The argument to the operator.
    # @param [Array] variables The variables the operator is working on.
    # @param [Hash] mapping The mapping of variables to values.
    #
    # @return [String] The extracted result.
    def extract_suffix_operator(
        value, processor, argument, variables, mapping)
      if variables.size != 1
        raise InvalidTemplateOperatorError,
          "Template operator 'suffix' takes exactly one variable."
      end
      if value[-argument.size..-1] != argument
        raise TemplateOperatorAbortedError,
          "Value for template operator 'suffix' missing expected suffix."
      end
      values = value.split(argument, -1)
      values.pop if values[-1] == ""
      if processor && processor.respond_to?(:restore)
        values.map! { |value| processor.restore(variables.first, value) }
      end
      values = values.first if values.size == 1
      if mapping[variables.first] == nil || mapping[variables.first] == values
        mapping[variables.first] = values
      else
        raise TemplateOperatorAbortedError,
          "Value mismatch for repeated variable."
      end
    end

    ##
    # Extracts a URI Template join operator.
    #
    # @param [String] value The unparsed value to extract from.
    # @param [#restore] processor The processor object.
    # @param [String] argument The argument to the operator.
    # @param [Array] variables The variables the operator is working on.
    # @param [Hash] mapping The mapping of variables to values.
    #
    # @return [String] The extracted result.
    def extract_join_operator(value, processor, argument, variables, mapping)
      unparsed_values = value.split(argument)
      parsed_variables = []
      for unparsed_value in unparsed_values
        name = unparsed_value[/^(.+?)=(.+)$/, 1]
        parsed_variables << name
        parsed_value = unparsed_value[/^(.+?)=(.+)$/, 2]
        if processor && processor.respond_to?(:restore)
          parsed_value = processor.restore(name, parsed_value)
        end
        if mapping[name] == nil || mapping[name] == parsed_value
          mapping[name] = parsed_value
        else
          raise TemplateOperatorAbortedError,
            "Value mismatch for repeated variable."
        end
      end
      for variable in variables
        if !parsed_variables.include?(variable) && mapping[variable] != nil
          raise TemplateOperatorAbortedError,
            "Value mismatch for repeated variable."
        end
      end
      if (parsed_variables & variables) != parsed_variables
        raise TemplateOperatorAbortedError,
          "Template operator 'join' variable mismatch: " +
          "#{parsed_variables.inspect}, #{variables.inspect}"
      end
    end

    ##
    # Extracts a URI Template list operator.
    #
    # @param [String] value The unparsed value to extract from.
    # @param [#restore] processor The processor object.
    # @param [String] argument The argument to the operator.
    # @param [Array] variables The variables the operator is working on.
    # @param [Hash] mapping The mapping of variables to values.
    #
    # @return [String] The extracted result.
    def extract_list_operator(value, processor, argument, variables, mapping)
      if variables.size != 1
        raise InvalidTemplateOperatorError,
          "Template operator 'list' takes exactly one variable."
      end
      values = value.split(argument, -1)
      values.pop if values[-1] == ""
      if processor && processor.respond_to?(:restore)
        values.map! { |value| processor.restore(variables.first, value) }
      end
      if mapping[variables.first] == nil || mapping[variables.first] == values
        mapping[variables.first] = values
      else
        raise TemplateOperatorAbortedError,
          "Value mismatch for repeated variable."
      end
    end
  end
end