VYPR
High severityNVD Advisory· Published Jul 6, 2021· Updated Aug 3, 2024

Regular Expression Denial of Service in Addressable templates

CVE-2021-32740

Description

Addressable is an alternative implementation to the URI implementation that is part of Ruby's standard library. An uncontrolled resource consumption vulnerability exists after version 2.3.0 through version 2.7.0. Within the URI template implementation in Addressable, a maliciously crafted template may result in uncontrolled resource consumption, leading to denial of service when matched against a URI. In typical usage, templates would not normally be read from untrusted user input, but nonetheless, no previous security advisory for Addressable has cautioned against doing this. Users of the parsing capabilities in Addressable but not the URI template capabilities are unaffected. The vulnerability is patched in version 2.8.0. As a workaround, only create Template objects from trusted sources that have been validated not to produce catastrophic backtracking.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
addressableRubyGems
>= 2.3.0, < 2.8.02.8.0

Affected products

1

Patches

3
0d8a3127e358

Adding note about ReDoS vulnerability

1 file changed · +1 0
  • CHANGELOG.md+1 0 modified
    @@ -1,4 +1,5 @@
     # Addressable 2.8.0
    +- fixes ReDoS vulnerability in Addressable::Template#match
     - no longer replaces `+` with spaces in queries for non-http(s) schemes
     - fixed encoding ipv6 literals
     - the `:compacted` flag for `normalized_query` now dedupes parameters
    
89c76130ce25

Merge branch 'template-regexp' into main

2 files changed · +10 1
  • lib/addressable/template.rb+1 1 modified
    @@ -37,7 +37,7 @@ class Template
           Addressable::URI::CharacterClasses::DIGIT + '_'
     
         var_char =
    -      "(?:(?:[#{variable_char_class}]|%[a-fA-F0-9][a-fA-F0-9])+)"
    +      "(?>(?:[#{variable_char_class}]|%[a-fA-F0-9][a-fA-F0-9])+)"
         RESERVED =
           "(?:[#{anything}]|%[a-fA-F0-9][a-fA-F0-9])"
         UNRESERVED =
    
  • spec/addressable/template_spec.rb+9 0 modified
    @@ -19,6 +19,7 @@
     require "spec_helper"
     
     require "bigdecimal"
    +require "timeout"
     require "addressable/template"
     
     shared_examples_for 'expands' do |tests|
    @@ -1340,6 +1341,14 @@ def self.match(name)
             expect(subject).not_to match("foo_bar*")
             expect(subject).not_to match("foo_bar:20")
           end
    +
    +      it 'should parse in a reasonable time' do
    +        expect do
    +          Timeout.timeout(0.1) do
    +            expect(subject).not_to match("0"*25 + "!")
    +          end
    +        end.not_to raise_error
    +      end
         end
         context "VARIABLE_LIST" do
           subject { Addressable::Template::VARIABLE_LIST }
    
92685096b1f7

Rename and remove subclass

https://github.com/sporkmonger/addressableDavid HaslemMay 19, 2012via ghsa
4 files changed · +999 4001
  • lib/addressable/template.rb+321 543 modified
    @@ -22,15 +22,44 @@
     module Addressable
       ##
       # This is an implementation of a URI template based on
    -  # <a href="http://tinyurl.com/uritemplatedraft03">URI Template draft 03</a>.
    +  # RFC 6570 (http://tools.ietf.org/html/rfc6570).
       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}]+))?\}/
    +
    +
    +    variable_char_class =
    +      Addressable::URI::CharacterClasses::ALPHA +
    +      Addressable::URI::CharacterClasses::DIGIT + ?_
    +
    +    var_char =
    +      "(?:(?:[#{variable_char_class}]|%[a-fA-F0-9][a-fA-F0-9])+)"
    +    RESERVED =
    +      "(?:[#{anything}]|%[a-fA-F0-9][a-fA-F0-9])"
    +    UNRESERVED =
    +      "(?:[#{
    +        Addressable::URI::CharacterClasses::UNRESERVED
    +      }]|%[a-fA-F0-9][a-fA-F0-9])"
    +    variable =
    +      "(?:#{var_char}(?:\\.?#{var_char})*)"
    +    varspec =
    +      "(?:(#{variable})(\\*|:\\d+)?)"
    +    VARNAME =
    +      /^#{variable}$/
    +    VARSPEC =
    +      /^#{varspec}$/
    +    VARIABLE_LIST =
    +      /^#{varspec}(?:,#{varspec})*$/
    +    operator =
    +      "+#./;?&=,!@|"
    +    EXPRESSION =
    +      /\{([#{operator}])?(#{varspec}(?:,#{varspec})*)\}/
    +
    +
    +    LEADERS = {?? => ??, ?/ => ?/, ?# => ?#, ?. => ?., ?; => ?;, ?& => ?&}
    +    JOINERS = {?? => ?&, ?. => ?., ?; => ?;, ?& => ?&, ?/ => ?/}
     
         ##
         # Raised if an invalid template value is supplied.
    @@ -148,7 +177,7 @@ def inspect
         #
         # @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`.
    @@ -210,7 +239,7 @@ def extract(uri, processor=nil)
         #
         # @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`.
    @@ -253,7 +282,7 @@ def extract(uri, processor=nil)
         #
         #   uri = Addressable::URI.parse("http://example.com/a/b/c/")
         #   match = Addressable::Template.new(
    -    #     "http://example.com/{first}/{second}/"
    +    #     "http://example.com/{first}/{+second}/"
         #   ).match(uri, ExampleProcessor)
         #   match.variables
         #   #=> ["first", "second"]
    @@ -262,7 +291,7 @@ def extract(uri, processor=nil)
         #
         #   uri = Addressable::URI.parse("http://example.com/a/b/c/")
         #   match = Addressable::Template.new(
    -    #     "http://example.com/{first}/{-list|/|second}/"
    +    #     "http://example.com/{first}{/second*}/"
         #   ).match(uri)
         #   match.variables
         #   #=> ["first", "second"]
    @@ -279,38 +308,56 @@ def match(uri, processor=nil)
     
           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
    +      elsif expansions.size > 0
    +        index = 0
    +        expansions.each do |expansion|
    +          _, operator, varlist = *expansion.match(EXPRESSION)
    +          varlist.split(',').each do |varspec|
    +            _, name, modifier = *varspec.match(VARSPEC)
    +            case operator
    +            when nil, ?+, ?#, ?/, ?.
    +              unparsed_value = unparsed_values[index]
    +              name = varspec[VARSPEC, 1]
    +              value = unparsed_value
    +              value = value.split(JOINERS[operator]) if value && modifier == ?*
    +            when ?;, ??, ?&
    +              if modifier == ?*
    +                value = unparsed_values[index].split(JOINERS[operator])
    +                value = value.inject({}) do |acc, v|
    +                  key, val = v.split('=')
    +                  val = "" if val.nil?
    +                  acc[key] = val
    +                  acc
    +                end
    +              else
    +                if (unparsed_values[index])
    +                  name, value = unparsed_values[index].split('=')
    +                  value = "" if value.nil?
    +                end
                   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 processor == nil
    +              if value.is_a?(Hash)
    +                value = value.inject({}){|acc, (k, v)|
    +                  acc[Addressable::URI.unencode_component(k)] =
    +                    Addressable::URI.unencode_component(v)
    +                  acc
    +                }
    +              elsif value.is_a?(Array)
    +                value = value.map{|v| Addressable::URI.unencode_component(v) }
    +              else
    +                value = Addressable::URI.unencode_component(value)
    +              end
    +            end
                 if mapping[name] == nil || mapping[name] == value
                   mapping[name] = value
                 else
                   return nil
                 end
    +            index = index + 1
               end
             end
             return Addressable::Template::MatchData.new(uri, self, mapping)
    @@ -324,7 +371,7 @@ def match(uri, processor=nil)
         #
         # @param [Hash] mapping The mapping that corresponds to the pattern.
         # @param [#validate, #transform] processor
    -    #   An optional processor object may be supplied. 
    +    #   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
    @@ -347,50 +394,18 @@ def match(uri, processor=nil)
         #   #=> "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}/"
    +    #     "http://example.com/{?one,two}/"
         #   ).partial_expand({"one" => "1"}).pattern
    -    #   #=> "http://example.com/?one=1{-prefix|&two=|two}"
    +    #   #=> "http://example.com/?one=1{&two}/"
         #
         #   Addressable::Template.new(
    -    #     "http://example.com/{-join|&|one,two,three}/"
    +    #     "http://example.com/{?one,two,three}/"
         #   ).partial_expand({"one" => "1", "three" => 3}).pattern
    -    #   #=> "http://example.com/?one=1{-prefix|&two=|two}&three=3"
    +    #   #=> "http://example.com/?one=1{&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
    +      result.gsub!( EXPRESSION ) do |capture|
    +        transform_partial_capture(mapping, capture, processor)
           end
           return Addressable::Template.new(result)
         end
    @@ -438,11 +453,11 @@ def partial_expand(mapping, processor=nil)
         #   #=> "http://example.com/search/an+example+search+query/"
         #
         #   Addressable::Template.new(
    -    #     "http://example.com/search/{-list|+|query}/"
    +    #     "http://example.com/search/{query}/"
         #   ).expand(
    -    #     {"query" => "an example search query".split(" ")}
    +    #     {"query" => "an example search query"}
         #   ).to_str
    -    #   #=> "http://example.com/search/an+example+search+query/"
    +    #   #=> "http://example.com/search/an%20example%20search%20query/"
         #
         #   Addressable::Template.new(
         #     "http://example.com/search/{query}/"
    @@ -453,24 +468,9 @@ def partial_expand(mapping, processor=nil)
         #   #=> 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
    +      mapping = normalize_keys(mapping)
    +      result.gsub!( EXPRESSION ) do |capture|
    +        transform_capture(mapping, capture, processor)
           end
           return Addressable::URI.parse(result)
         end
    @@ -499,27 +499,25 @@ def variable_defaults
     
       private
         def ordered_variable_defaults
    -      @ordered_variable_defaults ||= (begin
    +      @ordered_variable_defaults ||= (
             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]
    +        expansions.map do |capture|
    +          _, operator, varlist = *capture.match(EXPRESSION)
    +          varlist.split(',').map do |varspec|
    +            name = varspec[VARSPEC, 1]
               end
    -          result
    -        end
    -      end)
    +        end.flatten
    +      )
         end
     
    +
         ##
    -    # Transforms a mapping so that values can be substituted into the
    -    # template.
    +    # Loops through each capture and expands any values available in mapping
         #
    -    # @param [Hash] mapping The mapping of variables to values.
    +    # @param [Hash] mapping
    +    #   Set of keys to expand
    +    # @param [String] capture
    +    #   The expression to expand
         # @param [#validate, #transform] processor
         #   An optional processor object may be supplied.
         #
    @@ -535,279 +533,227 @@ def ordered_variable_defaults
         # 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) }
    +    # @return [String] The expanded expression
    +    def transform_partial_capture(mapping, capture, processor = nil)
    +      _, operator, varlist = *capture.match(EXPRESSION)
    +      is_first = true
    +      varlist.split(',').inject('') do |acc, varspec|
    +        _, name, modifier = *varspec.match(VARSPEC)
    +        value = mapping[name]
    +        if value
    +          operator = ?& if !is_first && operator == ??
    +          acc << transform_capture(mapping, "{#{operator}#{varspec}}", processor)
             else
    -          value = Addressable::IDNA.unicode_normalize_kc(value)
    +          operator = ?& if !is_first && operator == ??
    +          acc << "{#{operator}#{varspec}}"
             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
    +        is_first = false
    +        acc
           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.
    +    # Transforms a mapped value so that values can be substituted into the
    +    # template.
         #
    -    # @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 [Hash] mapping The mapping to replace captures
    +    # @param [String] capture
    +    #   The expression to replace
    +    # @param [#validate, #transform] processor
    +    #   An optional processor object may be supplied.
         #
    -    # @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.
    +    # 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 [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
    +    # @return [String] The expanded expression
    +    def transform_capture(mapping, capture, processor=nil)
    +      _, operator, varlist = *capture.match(EXPRESSION)
    +      return_value = varlist.split(',').inject([]) do |acc, varspec|
    +        _, name, modifier = *varspec.match(VARSPEC)
    +        value = mapping[name]
    +        unless value == nil || value == {}
    +          allow_reserved = %w(+ #).include?(operator)
    +          value = value.to_s if Numeric === value || Symbol === value
    +          length = modifier.gsub(':', '').to_i if modifier =~ /^:\d+/
    +
    +          unless (Hash === value) ||
    +            value.respond_to?(:to_ary) || value.respond_to?(:to_str)
    +            raise TypeError,
    +              "Can't convert #{value.class} into String or Array."
    +          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
    +          value = normalize_value(value)
     
    -    ##
    -    # 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])
    +          if processor == nil || !processor.respond_to?(:transform)
    +            # Handle percent escaping
    +            if allow_reserved
    +              encode_map =
    +                Addressable::URI::CharacterClasses::RESERVED +
    +                Addressable::URI::CharacterClasses::UNRESERVED
    +            else
    +              encode_map = Addressable::URI::CharacterClasses::UNRESERVED
                 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]))
    +            if value.kind_of?(Array)
    +              transformed_value = value.map do |val|
    +                if length
    +                  Addressable::URI.encode_component(val[0...length], encode_map)
    +                else
    +                  Addressable::URI.encode_component(val, encode_map)
    +                end
                   end
    -            else
    -              if !buffer.empty? && (buffer[-1..-1] != "}" || state == :prefix)
    -                buffer << "{-opt|#{argument}|#{variable}}"
    -                state = :prefix
    +              unless modifier == "*"
    +                transformed_value = transformed_value.join(',')
    +              end
    +            elsif value.kind_of?(Hash)
    +              transformed_value = value.map do |key, val|
    +                if modifier == "*"
    +                  "#{
    +                    Addressable::URI.encode_component( key, encode_map)
    +                  }=#{
    +                    Addressable::URI.encode_component( val, encode_map)
    +                  }"
    +                else
    +                  "#{
    +                    Addressable::URI.encode_component( key, encode_map)
    +                  },#{
    +                    Addressable::URI.encode_component( val, encode_map)
    +                  }"
    +                end
                   end
    -              if buffer.empty? && variables.size == 1
    -                # Evaluates back to itself
    -                buffer << "{-join|#{argument}|#{variable}}"
    +              unless modifier == "*"
    +                transformed_value = transformed_value.join(',')
    +              end
    +            else
    +              if length
    +                transformed_value = Addressable::URI.encode_component(
    +                  value[0...length], encode_map)
                   else
    -                buffer << "{-prefix|#{variable}=|#{variable}}"
    +                transformed_value = Addressable::URI.encode_component(
    +                  value, encode_map)
                   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
    +
    +          # 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
    -          else
    -            raise InvalidTemplateOperatorError,
    -              "Template operator 'join' does not accept Array values."
    +            if processor.respond_to?(:transform)
    +              transformed_value = processor.transform(name, value)
    +              transformed_value = normalize_value(transformed_value)
    +            end
               end
    +          acc << [name, transformed_value]
             end
    -        buffer
    +        acc
    +      end
    +      return "" if return_value.empty?
    +      join_values(operator, return_value)
    +    end
    +
    +    ##
    +    # Takes a set of values, and joins them together based on the
    +    # operator.
    +    #
    +    # @param [String, Nil] operator One of the operators from the set
    +    #   (?,&,+,#,;,/,.), or nil if there wasn't one.
    +    # @param [Array] return_value
    +    #   The set of return values (as [variable_name, value] tuples) that will
    +    #   be joined together.
    +    #
    +    # @return [String] The transformed mapped value
    +    def join_values(operator, return_value)
    +      leader = LEADERS.fetch(operator, '')
    +      joiner = JOINERS.fetch(operator, ',')
    +      case operator
    +      when ?&, ??
    +        leader + return_value.map{|k,v|
    +          if v.is_a?(Array) && v.first =~ /=/
    +            v.join(joiner)
    +          elsif v.is_a?(Array)
    +            v.map{|v| "#{k}=#{v}"}.join(joiner)
    +          else
    +            "#{k}=#{v}"
    +          end
    +        }.join(joiner)
    +      when ?;
    +        return_value.map{|k,v|
    +          if v.is_a?(Array) && v.first =~ /=/
    +            ?; + v.join(";")
    +          elsif v.is_a?(Array)
    +            ?; + v.map{|v| "#{k}=#{v}"}.join(";")
    +          else
    +            v && v != '' ?  ";#{k}=#{v}" : ";#{k}"
    +          end
    +        }.join
    +      else
    +        leader + return_value.map{|k,v| v}.join(joiner)
           end
         end
     
         ##
    -    # Expands a URI Template list operator.
    +    # Takes a set of values, and joins them together based on the
    +    # 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.
    +    # @param [Hash, Array, String] value
    +    #   Normalizes keys and values with IDNA#unicode_normalize_kc
         #
    -    # @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."
    +    # @return [Hash, Array, String] The normalized values
    +    def normalize_value(value)
    +      unless value.is_a?(Hash)
    +        value = value.respond_to?(:to_ary) ? value.to_ary : value.to_str
           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
    +
    +      # Handle unicode normalization
    +      if value.kind_of?(Array)
    +        value.map! { |val| Addressable::IDNA.unicode_normalize_kc(val) }
    +      elsif value.kind_of?(Hash)
    +        value = value.inject({}) { |acc, (k, v)|
    +          acc[Addressable::IDNA.unicode_normalize_kc(k)] =
    +            Addressable::IDNA.unicode_normalize_kc(v)
    +          acc
    +        }
           else
    -        "{-list|#{argument}|#{variables.first}}"
    +        value = Addressable::IDNA.unicode_normalize_kc(value)
           end
    +      value
         end
     
         ##
    -    # Parses a URI template expansion <tt>String</tt>.
    +    # Generates a hash with string keys
         #
    -    # @param [String] expansion The operator <tt>String</tt>.
    -    # @param [Hash] mapping An optional mapping to merge defaults into.
    +    # @param [Hash] mapping A mapping hash to normalize
         #
    -    # @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
    +    # @return [Hash]
    +    #   A hash with stringified keys
    +    def normalize_keys(mapping)
    +      return mapping.inject({}) do |accu, pair|
    +        name, value = pair
    +        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
    +        accu[name] = value
             accu
    -      end).merge(mapping)
    -      variables = variables.map { |var| var.gsub(/=.*$/, "") }
    -      return operator, argument, variables, mapping
    +      end
         end
     
         ##
    @@ -832,216 +778,48 @@ def parse_template_pattern(pattern, processor=nil)
     
           # 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|
    +      regexp_string = escaped_pattern.gsub( EXPRESSION ) do |expansion|
    +
             expansions << expansion
    -        if expansion =~ OPERATOR_EXPANSION
    -          capture_group = "(.*)"
    -          operator, argument, names, _ =
    -            parse_template_expansion(expansion)
    +        _, operator, varlist = *expansion.match(EXPRESSION)
    +        leader = Regexp.escape(LEADERS.fetch(operator, ''))
    +        joiner = Regexp.escape(JOINERS.fetch(operator, ','))
    +        leader + varlist.split(',').map do |varspec|
    +          _, name, modifier = *varspec.match(VARSPEC)
               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)})"
    +            "(#{ processor.match(name) })"
    +          else
    +            group = case operator
    +            when ?+
    +              "#{ RESERVED }*?"
    +            when ?#
    +              "#{ RESERVED }*?"
    +            when ?/
    +              "#{ UNRESERVED }*?"
    +            when ?.
    +              "#{ UNRESERVED.gsub('\.', '') }*?"
    +            when ?;
    +              "#{ UNRESERVED }*=?#{ UNRESERVED }*?"
    +            when ??
    +              "#{ UNRESERVED }*=#{ UNRESERVED }*?"
    +            when ?&
    +              "#{ UNRESERVED }*=#{ UNRESERVED }*?"
    +            else
    +              "#{ UNRESERVED }*?"
    +            end
    +            if modifier == ?*
    +              "(#{group}(?:#{joiner}?#{group})*)?"
    +            else
    +              "(#{group})?"
                 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.join("#{joiner}?")
           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! { |val| processor.restore(variables.first, val) }
    -      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! { |val| processor.restore(variables.first, val) }
    -      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! { |val| processor.restore(variables.first, val) }
    -      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
    
  • lib/addressable/uri_template.rb+0 636 removed
    @@ -1,636 +0,0 @@
    -# 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"
    -require "addressable/template"
    -
    -module Addressable
    -  ##
    -  # This is an implementation of a URI template based on
    -  # RFC 6570 (http://tools.ietf.org/html/rfc6570).
    -  class UriTemplate < Template
    -    # Constants used throughout the template code.
    -    anything =
    -      Addressable::URI::CharacterClasses::RESERVED +
    -      Addressable::URI::CharacterClasses::UNRESERVED
    -
    -
    -    variable_char_class =
    -      Addressable::URI::CharacterClasses::ALPHA +
    -      Addressable::URI::CharacterClasses::DIGIT + ?_
    -
    -    var_char =
    -      "(?:(?:[#{variable_char_class}]|%[a-fA-F0-9][a-fA-F0-9])+)"
    -    RESERVED =
    -      "(?:[#{anything}]|%[a-fA-F0-9][a-fA-F0-9])"
    -    UNRESERVED =
    -      "(?:[#{
    -        Addressable::URI::CharacterClasses::UNRESERVED
    -      }]|%[a-fA-F0-9][a-fA-F0-9])"
    -    variable =
    -      "(?:#{var_char}(?:\\.?#{var_char})*)"
    -    varspec =
    -      "(?:(#{variable})(\\*|:\\d+)?)"
    -    VARNAME =
    -      /^#{variable}$/
    -    VARSPEC =
    -      /^#{varspec}$/
    -    VARIABLE_LIST =
    -      /^#{varspec}(?:,#{varspec})*$/
    -    operator =
    -      "+#./;?&=,!@|"
    -    EXPRESSION =
    -      /\{([#{operator}])?(#{varspec}(?:,#{varspec})*)\}/
    -
    -
    -    LEADERS = {?? => ??, ?/ => ?/, ?# => ?#, ?. => ?., ?; => ?;, ?& => ?&}
    -    JOINERS = {?? => ?&, ?. => ?., ?; => ?;, ?& => ?&, ?/ => ?/}
    -
    -
    -    ##
    -    # 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::UriTemplate.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::UriTemplate.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::UriTemplate.new(
    -    #     "http://example.com/{first}{/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
    -        index = 0
    -        expansions.each do |expansion|
    -          _, operator, varlist = *expansion.match(EXPRESSION)
    -          varlist.split(',').each do |varspec|
    -            _, name, modifier = *varspec.match(VARSPEC)
    -            case operator
    -            when nil, ?+, ?#, ?/, ?.
    -              unparsed_value = unparsed_values[index]
    -              name = varspec[VARSPEC, 1]
    -              value = unparsed_value
    -              value = value.split(JOINERS[operator]) if value && modifier == ?*
    -            when ?;, ??, ?&
    -              if modifier == ?*
    -                value = unparsed_values[index].split(JOINERS[operator])
    -                value = value.inject({}) do |acc, v|
    -                  key, val = v.split('=')
    -                  val = "" if val.nil?
    -                  acc[key] = val
    -                  acc
    -                end
    -              else
    -                if (unparsed_values[index])
    -                  name, value = unparsed_values[index].split('=')
    -                  value = "" if value.nil?
    -                end
    -              end
    -            end
    -            if processor != nil && processor.respond_to?(:restore)
    -              value = processor.restore(name, value)
    -            end
    -            if processor == nil
    -              if value.is_a?(Hash)
    -                value = value.inject({}){|acc, (k, v)|
    -                  acc[Addressable::URI.unencode_component(k)] =
    -                    Addressable::URI.unencode_component(v)
    -                  acc
    -                }
    -              elsif value.is_a?(Array)
    -                value = value.map{|v| Addressable::URI.unencode_component(v) }
    -              else
    -                value = Addressable::URI.unencode_component(value)
    -              end
    -            end
    -            if mapping[name] == nil || mapping[name] == value
    -              mapping[name] = value
    -            else
    -              return nil
    -            end
    -            index = index + 1
    -          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::UriTemplate] The partially expanded URI template.
    -    #
    -    # @example
    -    #   Addressable::UriTemplate.new(
    -    #     "http://example.com/{one}/{two}/"
    -    #   ).partial_expand({"one" => "1"}).pattern
    -    #   #=> "http://example.com/1/{two}/"
    -    #
    -    #   Addressable::UriTemplate.new(
    -    #     "http://example.com/{?one,two}/"
    -    #   ).partial_expand({"one" => "1"}).pattern
    -    #   #=> "http://example.com/?one=1{&two}/"
    -    #
    -    #   Addressable::UriTemplate.new(
    -    #     "http://example.com/{?one,two,three}/"
    -    #   ).partial_expand({"one" => "1", "three" => 3}).pattern
    -    #   #=> "http://example.com/?one=1{&two}&three=3"
    -    def partial_expand(mapping, processor=nil)
    -      result = self.pattern.dup
    -      result.gsub!( EXPRESSION ) do |capture|
    -        transform_partial_capture(mapping, capture, processor)
    -      end
    -      return Addressable::UriTemplate.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::UriTemplate.new(
    -    #     "http://example.com/search/{query}/"
    -    #   ).expand(
    -    #     {"query" => "an example search query"},
    -    #     ExampleProcessor
    -    #   ).to_str
    -    #   #=> "http://example.com/search/an+example+search+query/"
    -    #
    -    #   Addressable::UriTemplate.new(
    -    #     "http://example.com/search/{query}/"
    -    #   ).expand(
    -    #     {"query" => "an example search query"}
    -    #   ).to_str
    -    #   #=> "http://example.com/search/an%20example%20search%20query/"
    -    #
    -    #   Addressable::UriTemplate.new(
    -    #     "http://example.com/search/{query}/"
    -    #   ).expand(
    -    #     {"query" => "bogus!"},
    -    #     ExampleProcessor
    -    #   ).to_str
    -    #   #=> Addressable::Template::InvalidTemplateValueError
    -    def expand(mapping, processor=nil)
    -      result = self.pattern.dup
    -      mapping = normalize_keys(mapping)
    -      result.gsub!( EXPRESSION ) do |capture|
    -        transform_capture(mapping, capture, processor)
    -      end
    -      return Addressable::URI.parse(result)
    -    end
    -
    -
    -  private
    -    def ordered_variable_defaults
    -      @ordered_variable_defaults ||= (
    -        expansions, expansion_regexp = parse_template_pattern(pattern)
    -        expansions.map do |capture|
    -          _, operator, varlist = *capture.match(EXPRESSION)
    -          varlist.split(',').map do |varspec|
    -            name = varspec[VARSPEC, 1]
    -          end
    -        end.flatten
    -      )
    -    end
    -
    -
    -    ##
    -    # Loops through each capture and expands any values available in mapping
    -    #
    -    # @param [Hash] mapping
    -    #   Set of keys to expand
    -    # @param [String] capture
    -    #   The expression to expand
    -    # @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 [String] The expanded expression
    -    def transform_partial_capture(mapping, capture, processor = nil)
    -      _, operator, varlist = *capture.match(EXPRESSION)
    -      is_first = true
    -      varlist.split(',').inject('') do |acc, varspec|
    -        _, name, modifier = *varspec.match(VARSPEC)
    -        value = mapping[name]
    -        if value
    -          operator = ?& if !is_first && operator == ??
    -          acc << transform_capture(mapping, "{#{operator}#{varspec}}", processor)
    -        else
    -          operator = ?& if !is_first && operator == ??
    -          acc << "{#{operator}#{varspec}}"
    -        end
    -        is_first = false
    -        acc
    -      end
    -    end
    -
    -    ##
    -    # Transforms a mapped value so that values can be substituted into the
    -    # template.
    -    #
    -    # @param [Hash] mapping The mapping to replace captures
    -    # @param [String] capture
    -    #   The expression to replace
    -    # @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 [String] The expanded expression
    -    def transform_capture(mapping, capture, processor=nil)
    -      _, operator, varlist = *capture.match(EXPRESSION)
    -      return_value = varlist.split(',').inject([]) do |acc, varspec|
    -        _, name, modifier = *varspec.match(VARSPEC)
    -        value = mapping[name]
    -        unless value == nil || value == {}
    -          allow_reserved = %w(+ #).include?(operator)
    -          value = value.to_s if Numeric === value || Symbol === value
    -          length = modifier.gsub(':', '').to_i if modifier =~ /^:\d+/
    -
    -          unless (Hash === value) ||
    -            value.respond_to?(:to_ary) || value.respond_to?(:to_str)
    -            raise TypeError,
    -              "Can't convert #{value.class} into String or Array."
    -          end
    -
    -          value = normalize_value(value)
    -
    -          if processor == nil || !processor.respond_to?(:transform)
    -            # Handle percent escaping
    -            if allow_reserved
    -              encode_map =
    -                Addressable::URI::CharacterClasses::RESERVED +
    -                Addressable::URI::CharacterClasses::UNRESERVED
    -            else
    -              encode_map = Addressable::URI::CharacterClasses::UNRESERVED
    -            end
    -            if value.kind_of?(Array)
    -              transformed_value = value.map do |val|
    -                if length
    -                  Addressable::URI.encode_component(val[0...length], encode_map)
    -                else
    -                  Addressable::URI.encode_component(val, encode_map)
    -                end
    -              end
    -              unless modifier == "*"
    -                transformed_value = transformed_value.join(',')
    -              end
    -            elsif value.kind_of?(Hash)
    -              transformed_value = value.map do |key, val|
    -                if modifier == "*"
    -                  "#{
    -                    Addressable::URI.encode_component( key, encode_map)
    -                  }=#{
    -                    Addressable::URI.encode_component( val, encode_map)
    -                  }"
    -                else
    -                  "#{
    -                    Addressable::URI.encode_component( key, encode_map)
    -                  },#{
    -                    Addressable::URI.encode_component( val, encode_map)
    -                  }"
    -                end
    -              end
    -              unless modifier == "*"
    -                transformed_value = transformed_value.join(',')
    -              end
    -            else
    -              if length
    -                transformed_value = Addressable::URI.encode_component(
    -                  value[0...length], encode_map)
    -              else
    -                transformed_value = Addressable::URI.encode_component(
    -                  value, encode_map)
    -              end
    -            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)
    -              transformed_value = normalize_value(transformed_value)
    -            end
    -          end
    -          acc << [name, transformed_value]
    -        end
    -        acc
    -      end
    -      return "" if return_value.empty?
    -      join_values(operator, return_value)
    -    end
    -
    -    ##
    -    # Takes a set of values, and joins them together based on the
    -    # operator.
    -    #
    -    # @param [String, Nil] operator One of the operators from the set
    -    #   (?,&,+,#,;,/,.), or nil if there wasn't one.
    -    # @param [Array] return_value
    -    #   The set of return values (as [variable_name, value] tuples) that will
    -    #   be joined together.
    -    #
    -    # @return [String] The transformed mapped value
    -    def join_values(operator, return_value)
    -      leader = LEADERS.fetch(operator, '')
    -      joiner = JOINERS.fetch(operator, ',')
    -      case operator
    -      when ?&, ??
    -        leader + return_value.map{|k,v|
    -          if v.is_a?(Array) && v.first =~ /=/
    -            v.join(joiner)
    -          elsif v.is_a?(Array)
    -            v.map{|v| "#{k}=#{v}"}.join(joiner)
    -          else
    -            "#{k}=#{v}"
    -          end
    -        }.join(joiner)
    -      when ?;
    -        return_value.map{|k,v|
    -          if v.is_a?(Array) && v.first =~ /=/
    -            ?; + v.join(";")
    -          elsif v.is_a?(Array)
    -            ?; + v.map{|v| "#{k}=#{v}"}.join(";")
    -          else
    -            v && v != '' ?  ";#{k}=#{v}" : ";#{k}"
    -          end
    -        }.join
    -      else
    -        leader + return_value.map{|k,v| v}.join(joiner)
    -      end
    -    end
    -
    -    ##
    -    # Takes a set of values, and joins them together based on the
    -    # operator.
    -    #
    -    # @param [Hash, Array, String] value
    -    #   Normalizes keys and values with IDNA#unicode_normalize_kc
    -    #
    -    # @return [Hash, Array, String] The normalized values
    -    def normalize_value(value)
    -      unless value.is_a?(Hash)
    -        value = value.respond_to?(:to_ary) ? value.to_ary : value.to_str
    -      end
    -
    -      # Handle unicode normalization
    -      if value.kind_of?(Array)
    -        value.map! { |val| Addressable::IDNA.unicode_normalize_kc(val) }
    -      elsif value.kind_of?(Hash)
    -        value = value.inject({}) { |acc, (k, v)|
    -          acc[Addressable::IDNA.unicode_normalize_kc(k)] =
    -            Addressable::IDNA.unicode_normalize_kc(v)
    -          acc
    -        }
    -      else
    -        value = Addressable::IDNA.unicode_normalize_kc(value)
    -      end
    -      value
    -    end
    -
    -    ##
    -    # Generates a hash with string keys
    -    #
    -    # @param [Hash] mapping A mapping hash to normalize
    -    #
    -    # @return [Hash]
    -    #   A hash with stringified keys
    -    def normalize_keys(mapping)
    -      return mapping.inject({}) do |accu, pair|
    -        name, value = pair
    -        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
    -        accu[name] = value
    -        accu
    -      end
    -    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( EXPRESSION ) do |expansion|
    -
    -        expansions << expansion
    -        _, operator, varlist = *expansion.match(EXPRESSION)
    -        leader = Regexp.escape(LEADERS.fetch(operator, ''))
    -        joiner = Regexp.escape(JOINERS.fetch(operator, ','))
    -        leader + varlist.split(',').map do |varspec|
    -          _, name, modifier = *varspec.match(VARSPEC)
    -          if processor != nil && processor.respond_to?(:match)
    -            "(#{ processor.match(name) })"
    -          else
    -            group = case operator
    -            when ?+
    -              "#{ RESERVED }*?"
    -            when ?#
    -              "#{ RESERVED }*?"
    -            when ?/
    -              "#{ UNRESERVED }*?"
    -            when ?.
    -              "#{ UNRESERVED.gsub('\.', '') }*?"
    -            when ?;
    -              "#{ UNRESERVED }*=?#{ UNRESERVED }*?"
    -            when ??
    -              "#{ UNRESERVED }*=#{ UNRESERVED }*?"
    -            when ?&
    -              "#{ UNRESERVED }*=#{ UNRESERVED }*?"
    -            else
    -              "#{ UNRESERVED }*?"
    -            end
    -            if modifier == ?*
    -              "(#{group}(?:#{joiner}?#{group})*)?"
    -            else
    -              "(#{group})?"
    -            end
    -          end
    -        end.join("#{joiner}?")
    -      end
    -
    -      # Ensure that the regular expression matches the whole URI.
    -      regexp_string = "^#{regexp_string}$"
    -      return expansions, Regexp.new(regexp_string)
    -    end
    -
    -  end
    -end
    
  • spec/addressable/template_spec.rb+678 2064 modified
    @@ -1,2144 +1,758 @@
    -# coding: 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/template"
     
    -if !"".respond_to?("force_encoding")
    -  class String
    -    def force_encoding(encoding)
    -      @encoding = encoding
    -    end
    -
    -    def encoding
    -      @encoding ||= Encoding::ASCII_8BIT
    -    end
    -  end
    -
    -  class Encoding
    -    def initialize(name)
    -      @name = name
    -    end
    -
    -    def to_s
    -      return @name
    +shared_examples_for 'expands' do |tests|
    +  tests.each do |template, expansion|
    +    it "#{template} to #{expansion}" do
    +      tmpl = Addressable::Template.new(template).expand(subject)
    +      tmpl.to_str.should == expansion
         end
    -
    -    UTF_8 = Encoding.new("UTF-8")
    -    ASCII_8BIT = Encoding.new("US-ASCII")
    -  end
    -end
    -
    -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
    -
    -  def self.restore(name, value)
    -    return value.gsub(/\+/, " ") if name == "query"
    -    return value.tr("A-Za-z", "N-ZA-Mn-za-m") if name == "rot13"
    -    return value
    -  end
    -
    -  def self.match(name)
    -    return ".*?" if name == "first"
    -    return ".*"
    -  end
    -end
    -
    -class SlashlessProcessor
    -  def self.match(name)
    -    return "[^/\\n]*"
    -  end
    -end
    -
    -class NoOpProcessor
    -  def self.transform(name, value)
    -    value
    -  end
    -end
    -
    -describe Addressable::Template do
    -  it "should raise a TypeError for invalid patterns" do
    -    (lambda do
    -      Addressable::Template.new(42)
    -    end).should raise_error(TypeError, "Can't convert Fixnum into String.")
    -  end
    -end
    -
    -describe Addressable::Template, "created with the pattern '/'" do
    -  before do
    -    @template = Addressable::Template.new("/")
    -  end
    -
    -  it "should have no variables" do
    -    @template.variables.should be_empty
    -  end
    -
    -  it "should have the correct mapping when extracting from '/'" do
    -    @template.extract("/").should == {}
    -  end
    -end
    -
    -describe Addressable::URI, "when parsed from '/one/'" do
    -  before do
    -    @uri = Addressable::URI.parse("/one/")
    -  end
    -
    -  it "should not match the pattern '/two/'" do
    -    Addressable::Template.new("/two/").extract(@uri).should == nil
    -  end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern '/{number}/'" do
    -    Addressable::Template.new(
    -      "/{number}/"
    -    ).extract(@uri).should == {"number" => "one"}
    -  end
    -end
    -
    -describe Addressable::Template, "created with the pattern '/{number}/'" do
    -  before do
    -    @template = Addressable::Template.new("/{number}/")
    -  end
    -
    -  it "should have the variables ['number']" do
    -    @template.variables.should == ["number"]
    -  end
    -
    -  it "should not match the pattern '/'" do
    -    @template.match("/").should == nil
    -  end
    -
    -  it "should match the pattern '/two/'" do
    -    @template.match("/two/").mapping.should == {"number" => "two"}
    -  end
    -end
    -
    -describe Addressable::Template,
    -    "created with the pattern '/{number}/{number}/'" do
    -  before do
    -    @template = Addressable::Template.new("/{number}/{number}/")
    -  end
    -
    -  it "should have one variable" do
    -    @template.variables.should == ["number"]
    -  end
    -
    -  it "should have the correct mapping when extracting from '/1/1/'" do
    -    @template.extract("/1/1/").should == {"number" => "1"}
    -  end
    -
    -  it "should not match '/1/2/'" do
    -    @template.match("/1/2/").should == nil
    -  end
    -
    -  it "should not match '/2/1/'" do
    -    @template.match("/2/1/").should == nil
    -  end
    -
    -  it "should not match '/1/'" do
    -    @template.match("/1/").should == nil
    -  end
    -
    -  it "should not match '/1/1/1/'" do
    -    @template.match("/1/1/1/").should == nil
    -  end
    -
    -  it "should not match '/1/2/3/'" do
    -    @template.match("/1/2/3/").should == nil
       end
     end
     
    -describe Addressable::Template,
    -    "created with the pattern '/{number}{-prefix|.|number}'" do
    -  before do
    -    @template = Addressable::Template.new("/{number}{-prefix|.|number}")
    -  end
    -
    -  it "should have one variable" do
    -    @template.variables.should == ["number"]
    -  end
    -
    -  it "should have the correct mapping when extracting from '/1.1'" do
    -    @template.extract("/1.1").should == {"number" => "1"}
    -  end
    -
    -  it "should have the correct mapping when extracting from '/99.99'" do
    -    @template.extract("/99.99").should == {"number" => "99"}
    -  end
    -
    -  it "should not match '/1.2'" do
    -    @template.match("/1.2").should == nil
    -  end
    -
    -  it "should not match '/2.1'" do
    -    @template.match("/2.1").should == nil
    -  end
    -
    -  it "should not match '/1'" do
    -    @template.match("/1").should == nil
    -  end
    -
    -  it "should not match '/1.1.1'" do
    -    @template.match("/1.1.1").should == nil
    -  end
    -
    -  it "should not match '/1.23'" do
    -    @template.match("/1.23").should == nil
    -  end
    +describe "Level 1:" do
    +  subject{
    +    {:var => "value", :hello => "Hello World!"}
    +  }
    +  it_behaves_like 'expands', {
    +    '{var}' => 'value',
    +    '{hello}' => 'Hello%20World%21'
    +  }
     end
     
    -describe Addressable::Template,
    -    "created with the pattern '/{number}/{-suffix|/|number}'" do
    -  before do
    -    @template = Addressable::Template.new("/{number}/{-suffix|/|number}")
    -  end
    -
    -  it "should have one variable" do
    -    @template.variables.should == ["number"]
    -  end
    -
    -  it "should have the correct mapping when extracting from '/1/1/'" do
    -    @template.extract("/1/1/").should == {"number" => "1"}
    -  end
    -
    -  it "should have the correct mapping when extracting from '/99/99/'" do
    -    @template.extract("/99/99/").should == {"number" => "99"}
    -  end
    -
    -  it "should not match '/1/1'" do
    -    @template.match("/1/1").should == nil
    -  end
    -
    -  it "should not match '/11/'" do
    -    @template.match("/11/").should == nil
    -  end
    -
    -  it "should not match '/1/2/'" do
    -    @template.match("/1/2/").should == nil
    -  end
    -
    -  it "should not match '/2/1/'" do
    -    @template.match("/2/1/").should == nil
    -  end
    -
    -  it "should not match '/1/'" do
    -    @template.match("/1/").should == nil
    -  end
    -
    -  it "should not match '/1/1/1/'" do
    -    @template.match("/1/1/1/").should == nil
    +describe "Level 2" do
    +  subject{
    +    {
    +      :var => "value",
    +      :hello => "Hello World!",
    +      :path => "/foo/bar"
    +    }
    +  }
    +  context "Operator +:" do
    +    it_behaves_like 'expands', {
    +      '{+var}' => 'value',
    +      '{+hello}' => 'Hello%20World!',
    +      '{+path}/here' => '/foo/bar/here',
    +      'here?ref={+path}' => 'here?ref=/foo/bar'
    +    }
       end
    -
    -  it "should not match '/1/23/'" do
    -    @template.match("/1/23/").should == nil
    +  context "Operator #:" do
    +    it_behaves_like 'expands', {
    +      'X{#var}' => 'X#value',
    +      'X{#hello}' => 'X#Hello%20World!',
    +    }
       end
     end
     
    -describe Addressable::Template,
    -    "created with the pattern '/{number}/?{-join|&|number}'" do
    -  before do
    -    @template = Addressable::Template.new(
    -      "/{number}/?{-join|&|number,letter}"
    -    )
    -  end
    -
    -  it "should have one variable" do
    -    @template.variables.should == ["number", "letter"]
    -  end
    -
    -  it "should have the correct mapping when extracting from '/1/?number=1'" do
    -    @template.extract("/1/?number=1").should == {"number" => "1"}
    -  end
    -
    -  it "should have the correct mapping when extracting " +
    -      "from '/99/?number=99'" do
    -    @template.extract("/99/?number=99").should == {"number" => "99"}
    -  end
    -
    -  it "should have the correct mapping when extracting " +
    -      "from '/1/?number=1&letter=a'" do
    -    @template.extract("/1/?number=1&letter=a").should == {
    -      "number" => "1", "letter" => "a"
    +describe "Level 3" do
    +  subject{
    +    {
    +      :var => "value",
    +      :hello => "Hello World!",
    +      :empty => "",
    +      :path => "/foo/bar",
    +      :x => "1024",
    +      :y => "768"
    +    }
    +  }
    +  context "Operator nil (multiple vars):" do
    +    it_behaves_like 'expands', {
    +      'map?{x,y}' => 'map?1024,768',
    +      '{x,hello,y}' => '1024,Hello%20World%21,768'
         }
       end
    -
    -  it "should not match '/1/?number=1&bogus=foo'" do
    -    @template.match("/1/?number=1&bogus=foo").should == nil
    -  end
    -
    -  it "should not match '/1/?number=2'" do
    -    @template.match("/1/?number=2").should == nil
    -  end
    -
    -  it "should not match '/2/?number=1'" do
    -    @template.match("/2/?number=1").should == nil
    -  end
    -
    -  it "should not match '/1/?'" do
    -    @template.match("/1/?").should == nil
    -  end
    -end
    -
    -describe Addressable::Template,
    -    "created with the pattern '/{number}/{-list|/|number}/'" do
    -  before do
    -    @template = Addressable::Template.new("/{number}/{-list|/|number}/")
    +  context "Operator + (multiple vars):" do
    +    it_behaves_like 'expands', {
    +      '{+x,hello,y}' => '1024,Hello%20World!,768',
    +      '{+path,x}/here' => '/foo/bar,1024/here',
    +    }
       end
    -
    -  it "should have one variable" do
    -    @template.variables.should == ["number"]
    +  context "Operator # (multiple vars):" do
    +    it_behaves_like 'expands', {
    +      '{#x,hello,y}' => '#1024,Hello%20World!,768',
    +      '{#path,x}/here' => '#/foo/bar,1024/here',
    +    }
       end
    -
    -  it "should not match '/1/1/'" do
    -    @template.match("/1/1/").should == nil
    +  context "Operator ." do
    +    it_behaves_like 'expands', {
    +      'X{.var}' => 'X.value',
    +      'X{.x,y}' => 'X.1024.768',
    +    }
       end
    -
    -  it "should not match '/1/2/'" do
    -    @template.match("/1/2/").should == nil
    +  context "Operator /" do
    +    it_behaves_like 'expands', {
    +      '{/var}' => '/value',
    +      '{/var,x}/here' => '/value/1024/here',
    +    }
       end
    -
    -  it "should not match '/2/1/'" do
    -    @template.match("/2/1/").should == nil
    +  context "Operator ;" do
    +    it_behaves_like 'expands', {
    +      '{;x,y}' => ';x=1024;y=768',
    +      '{;x,y,empty}' => ';x=1024;y=768;empty',
    +    }
       end
    -
    -  it "should not match '/1/1/1/'" do
    -    @template.match("/1/1/1/").should == nil
    +  context "Operator ?" do
    +    it_behaves_like 'expands', {
    +      '{?x,y}' => '?x=1024&y=768',
    +      '{?x,y,empty}' => '?x=1024&y=768&empty=',
    +    }
       end
    -
    -  it "should not match '/1/1/1/1/'" do
    -    @template.match("/1/1/1/1/").should == nil
    +  context "Operator &" do
    +    it_behaves_like 'expands', {
    +      '?fixed=yes{&x}' => '?fixed=yes&x=1024',
    +      '{&x,y,empty}' => '&x=1024&y=768&empty=',
    +    }
       end
     end
    -
    -describe Addressable::Template, "created with the pattern " +
    -    "'http://www.example.com/?{-join|&|query,number}'" do
    -  before do
    -    @template = Addressable::Template.new(
    -      "http://www.example.com/?{-join|&|query,number}"
    -    )
    -  end
    -
    -  it "when inspected, should have the correct class name" do
    -    @template.inspect.should include("Addressable::Template")
    -  end
    -
    -  it "when inspected, should have the correct object id" do
    -    @template.inspect.should include("%#0x" % @template.object_id)
    -  end
    -
    -  it "should have the variables ['query', 'number']" do
    -    @template.variables.should == ["query", "number"]
    -  end
    -
    -  it "should not match the pattern 'http://www.example.com/'" do
    -    @template.match("http://www.example.com/").should == nil
    +describe "Level 4" do
    +  subject{
    +    {
    +      :var => "value",
    +      :hello => "Hello World!",
    +      :path => "/foo/bar",
    +      :semi => ";",
    +      :list => %w(red green blue),
    +      :keys => {"semi" => ';', "dot" => '.', "comma" => ','}
    +    }
    +  }
    +  context "Expansion with value modifiers" do
    +    it_behaves_like 'expands', {
    +      '{var:3}' => 'val',
    +      '{var:30}' => 'value',
    +      '{list}' => 'red,green,blue',
    +      '{list*}' => 'red,green,blue',
    +      '{keys}' => 'semi,%3B,dot,.,comma,%2C',
    +      '{keys*}' => 'semi=%3B,dot=.,comma=%2C',
    +    }
       end
    -
    -  it "should match the pattern 'http://www.example.com/?'" do
    -    @template.match("http://www.example.com/?").mapping.should == {}
    +  context "Operator + with value modifiers" do
    +    it_behaves_like 'expands', {
    +      '{+path:6}/here' => '/foo/b/here',
    +      '{+list}' => 'red,green,blue',
    +      '{+list*}' => 'red,green,blue',
    +      '{+keys}' => 'semi,;,dot,.,comma,,',
    +      '{+keys*}' => 'semi=;,dot=.,comma=,',
    +    }
       end
    -
    -  it "should match the pattern " +
    -      "'http://www.example.com/?query=mycelium'" do
    -    match = @template.match(
    -      "http://www.example.com/?query=mycelium"
    -    )
    -    match.variables.should == ["query", "number"]
    -    match.values.should == ["mycelium", nil]
    -    match.mapping.should == {"query" => "mycelium"}
    -    match.inspect.should =~ /MatchData/
    +  context "Operator # with value modifiers" do
    +    it_behaves_like 'expands', {
    +      '{#path:6}/here' => '#/foo/b/here',
    +      '{#list}' => '#red,green,blue',
    +      '{#list*}' => '#red,green,blue',
    +      '{#keys}' => '#semi,;,dot,.,comma,,',
    +      '{#keys*}' => '#semi=;,dot=.,comma=,',
    +    }
       end
    -
    -  it "should match the pattern " +
    -      "'http://www.example.com/?query=mycelium&number=100'" do
    -    @template.match(
    -      "http://www.example.com/?query=mycelium&number=100"
    -    ).mapping.should == {"query" => "mycelium", "number" => "100"}
    +  context "Operator . with value modifiers" do
    +    it_behaves_like 'expands', {
    +      'X{.var:3}' => 'X.val',
    +      'X{.list}' => 'X.red,green,blue',
    +      'X{.list*}' => 'X.red.green.blue',
    +      'X{.keys}' => 'X.semi,%3B,dot,.,comma,%2C',
    +      'X{.keys*}' => 'X.semi=%3B.dot=..comma=%2C',
    +    }
       end
    -end
    -
    -describe Addressable::URI, "when parsed from '/one/two/'" do
    -  before do
    -    @uri = Addressable::URI.parse("/one/two/")
    +  context "Operator / with value modifiers" do
    +    it_behaves_like 'expands', {
    +      '{/var:1,var}' => '/v/value',
    +      '{/list}' => '/red,green,blue',
    +      '{/list*}' => '/red/green/blue',
    +      '{/list*,path:4}' => '/red/green/blue/%2Ffoo',
    +      '{/keys}' => '/semi,%3B,dot,.,comma,%2C',
    +      '{/keys*}' => '/semi=%3B/dot=./comma=%2C',
    +    }
       end
    -
    -  it "should not match the pattern '/{number}/' " +
    -      "with the SlashlessProcessor" do
    -    Addressable::Template.new(
    -      "/{number}/"
    -    ).extract(@uri, SlashlessProcessor).should == nil
    +  context "Operator ; with value modifiers" do
    +    it_behaves_like 'expands', {
    +      '{;hello:5}' => ';hello=Hello',
    +      '{;list}' => ';list=red,green,blue',
    +      '{;list*}' => ';list=red;list=green;list=blue',
    +      '{;keys}' => ';keys=semi,%3B,dot,.,comma,%2C',
    +      '{;keys*}' => ';semi=%3B;dot=.;comma=%2C',
    +    }
       end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern '/{number}/' without a processor" do
    -    Addressable::Template.new("/{number}/").extract(@uri).should == {
    -      "number" => "one/two"
    +  context "Operator ? with value modifiers" do
    +    it_behaves_like 'expands', {
    +      '{?var:3}' => '?var=val',
    +      '{?list}' => '?list=red,green,blue',
    +      '{?list*}' => '?list=red&list=green&list=blue',
    +      '{?keys}' => '?keys=semi,%3B,dot,.,comma,%2C',
    +      '{?keys*}' => '?semi=%3B&dot=.&comma=%2C',
         }
       end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern '/{first}/{second}/' with the SlashlessProcessor" do
    -    Addressable::Template.new(
    -      "/{first}/{second}/"
    -    ).extract(@uri, SlashlessProcessor).should == {
    -      "first" => "one",
    -      "second" => "two"
    +  context "Operator & with value modifiers" do
    +    it_behaves_like 'expands', {
    +      '{&var:3}' => '&var=val',
    +      '{&list}' => '&list=red,green,blue',
    +      '{&list*}' => '&list=red&list=green&list=blue',
    +      '{&keys}' => '&keys=semi,%3B,dot,.,comma,%2C',
    +      '{&keys*}' => '&semi=%3B&dot=.&comma=%2C',
         }
       end
     end
    -
    -describe Addressable::URI, "when parsed from " +
    -    "'http://example.com/search/an+example+search+query/'" do
    -  before do
    -    @uri = Addressable::URI.parse(
    -      "http://example.com/search/an+example+search+query/")
    -  end
    -
    -  it "should have the correct mapping when extracting values using " +
    -      "the pattern 'http://example.com/search/{query}/' with the " +
    -      "ExampleProcessor" do
    -    Addressable::Template.new(
    -      "http://example.com/search/{query}/"
    -    ).extract(@uri, ExampleProcessor).should == {
    -      "query" => "an example search query"
    +describe "Modifiers" do
    +  subject{
    +    {
    +      :var => "value",
    +      :semi => ";",
    +      :year => %w(1965 2000 2012),
    +      :dom => %w(example com)
         }
    -  end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/search/{-list|+|query}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/search/{-list|+|query}/"
    -    ).extract(@uri).should == {
    -      "query" => ["an", "example", "search", "query"]
    +  }
    +  context "length" do
    +    it_behaves_like 'expands', {
    +      '{var:3}' => 'val',
    +      '{var:30}' => 'value',
    +      '{var}' => 'value',
    +      '{semi}' => '%3B',
    +      '{semi:2}' => '%3B',
         }
       end
    -
    -  it "should return nil when extracting values using " +
    -      "a non-matching pattern" do
    -    Addressable::Template.new(
    -      "http://bogus.com/{thingy}/"
    -    ).extract(@uri).should == nil
    +  context "explode" do
    +    it_behaves_like 'expands', {
    +      'find{?year*}' => 'find?year=1965&year=2000&year=2012',
    +      'www{.dom*}' => 'www.example.com',
    +    }
       end
     end
    -
    -describe Addressable::URI, "when parsed from " +
    -    "'http://example.com/a/b/c/'" do
    -  before do
    -    @uri = Addressable::URI.parse(
    -      "http://example.com/a/b/c/")
    -  end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/{first}/{second}/' with the ExampleProcessor" do
    -    Addressable::Template.new(
    -      "http://example.com/{first}/{second}/"
    -    ).extract(@uri, ExampleProcessor).should == {
    -      "first" => "a",
    -      "second" => "b/c"
    +describe "Expansion" do
    +  subject{
    +    {
    +      :count => [ "one", "two", "three" ],
    +      :dom => [ "example", "com" ],
    +      :dub   => "me/too",
    +      :hello => "Hello World!",
    +      :half  => "50%",
    +      :var   => "value",
    +      :who   => "fred",
    +      :base  => "http://example.com/home/",
    +      :path  => "/foo/bar",
    +      :list  => [ "red", "green", "blue" ],
    +      :keys  => { "semi"=>";","dot"=>".","comma"=>"," },
    +      :v     => "6",
    +      :x     => "1024",
    +      :y     => "768",
    +      :empty => "",
    +      :empty_keys  => {},
    +      :undef => nil,
         }
    -  end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/{first}/{-list|/|second}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{first}/{-list|/|second}/"
    -    ).extract(@uri).should == {
    -      "first" => "a",
    -      "second" => ["b", "c"]
    +  }
    +  context "concatenation" do
    +    it_behaves_like 'expands', {
    +      '{count}' => 'one,two,three',
    +      '{count*}' => 'one,two,three',
    +      '{/count}' => '/one,two,three',
    +      '{/count*}' => '/one/two/three',
    +      '{;count}' => ';count=one,two,three',
    +      '{;count*}' => ';count=one;count=two;count=three',
    +      '{?count}' => '?count=one,two,three',
    +      '{?count*}' => '?count=one&count=two&count=three',
    +      '{&count*}' => '&count=one&count=two&count=three',
         }
       end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/{first}/{-list|/|rot13}/' " +
    -      "with the ExampleProcessor" do
    -    Addressable::Template.new(
    -      "http://example.com/{first}/{-list|/|rot13}/"
    -    ).extract(@uri, ExampleProcessor).should == {
    -      "first" => "a",
    -      "rot13" => ["o", "p"]
    +  context "simple expansion" do
    +    it_behaves_like 'expands', {
    +      '{var}' => 'value',
    +      '{hello}' => 'Hello%20World%21',
    +      '{half}' => '50%25',
    +      'O{empty}X' => 'OX',
    +      'O{undef}X' => 'OX',
    +      '{x,y}' => '1024,768',
    +      '{x,hello,y}' => '1024,Hello%20World%21,768',
    +      '?{x,empty}' => '?1024,',
    +      '?{x,undef}' => '?1024',
    +      '?{undef,y}' => '?768',
    +      '{var:3}' => 'val',
    +      '{var:30}' => 'value',
    +      '{list}' => 'red,green,blue',
    +      '{list*}' => 'red,green,blue',
    +      '{keys}' => 'semi,%3B,dot,.,comma,%2C',
    +      '{keys*}' => 'semi=%3B,dot=.,comma=%2C',
         }
       end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/{-list|/|rot13}/' " +
    -      "with the ExampleProcessor" do
    -    Addressable::Template.new(
    -      "http://example.com/{-list|/|rot13}/"
    -    ).extract(@uri, ExampleProcessor).should == {
    -      "rot13" => ["n", "o", "p"]
    +  context "reserved expansion (+)" do
    +    it_behaves_like 'expands', {
    +      '{+var}' => 'value',
    +      '{+hello}' => 'Hello%20World!',
    +      '{+half}' => '50%25',
    +      '{base}index' => 'http%3A%2F%2Fexample.com%2Fhome%2Findex',
    +      '{+base}index' => 'http://example.com/home/index',
    +      'O{+empty}X' => 'OX',
    +      'O{+undef}X' => 'OX',
    +      '{+path}/here' => '/foo/bar/here',
    +      'here?ref={+path}' => 'here?ref=/foo/bar',
    +      'up{+path}{var}/here' => 'up/foo/barvalue/here',
    +      '{+x,hello,y}' => '1024,Hello%20World!,768',
    +      '{+path,x}/here' => '/foo/bar,1024/here',
    +      '{+path:6}/here' => '/foo/b/here',
    +      '{+list}' => 'red,green,blue',
    +      '{+list*}' => 'red,green,blue',
    +      '{+keys}' => 'semi,;,dot,.,comma,,',
    +      '{+keys*}' => 'semi=;,dot=.,comma=,',
         }
       end
    -
    -  it "should not map to anything when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/{-list|/|rot13}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{-join|/|a,b,c}/"
    -    ).extract(@uri).should == nil
    -  end
    -end
    -
    -describe Addressable::URI, "when parsed from " +
    -    "'http://example.com/?a=one&b=two&c=three'" do
    -  before do
    -    @uri = Addressable::URI.parse("http://example.com/?a=one&b=two&c=three")
    -  end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/?{-join|&|a,b,c}'" do
    -    Addressable::Template.new(
    -      "http://example.com/?{-join|&|a,b,c}"
    -    ).extract(@uri).should == {
    -      "a" => "one",
    -      "b" => "two",
    -      "c" => "three"
    +  context "fragment expansion (#)" do
    +    it_behaves_like 'expands', {
    +      '{#var}' => '#value',
    +      '{#hello}' => '#Hello%20World!',
    +      '{#half}' => '#50%25',
    +      'foo{#empty}' => 'foo#',
    +      'foo{#undef}' => 'foo',
    +      '{#x,hello,y}' => '#1024,Hello%20World!,768',
    +      '{#path,x}/here' => '#/foo/bar,1024/here',
    +      '{#path:6}/here' => '#/foo/b/here',
    +      '{#list}' => '#red,green,blue',
    +      '{#list*}' => '#red,green,blue',
    +      '{#keys}' => '#semi,;,dot,.,comma,,',
    +      '{#keys*}' => '#semi=;,dot=.,comma=,',
         }
       end
    -end
    -
    -describe Addressable::URI, "when parsed from " +
    -    "'http://example.com/?rot13=frperg'" do
    -  before do
    -    @uri = Addressable::URI.parse("http://example.com/?rot13=frperg")
    -  end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/?{-join|&|rot13}' with the ExampleProcessor" do
    -    Addressable::Template.new(
    -      "http://example.com/?{-join|&|rot13}"
    -    ).extract(@uri, ExampleProcessor).should == {
    -      "rot13" => "secret"
    +  context "label expansion (.)" do
    +    it_behaves_like 'expands', {
    +      '{.who}' => '.fred',
    +      '{.who,who}' => '.fred.fred',
    +      '{.half,who}' => '.50%25.fred',
    +      'www{.dom*}' => 'www.example.com',
    +      'X{.var}' => 'X.value',
    +      'X{.empty}' => 'X.',
    +      'X{.undef}' => 'X',
    +      'X{.var:3}' => 'X.val',
    +      'X{.list}' => 'X.red,green,blue',
    +      'X{.list*}' => 'X.red.green.blue',
    +      'X{.keys}' => 'X.semi,%3B,dot,.,comma,%2C',
    +      'X{.keys*}' => 'X.semi=%3B.dot=..comma=%2C',
    +      'X{.empty_keys}' => 'X',
    +      'X{.empty_keys*}' => 'X',
         }
       end
    -end
    -
    -describe Addressable::URI, "when parsed from " +
    -    "'http://example.org///something///'" do
    -  before do
    -    @uri = Addressable::URI.parse("http://example.org///something///")
    +  context "path expansion (/)" do
    +    it_behaves_like 'expands', {
    +      '{/who}' => '/fred',
    +      '{/who,who}' => '/fred/fred',
    +      '{/half,who}' => '/50%25/fred',
    +      '{/who,dub}' => '/fred/me%2Ftoo',
    +      '{/var}' => '/value',
    +      '{/var,empty}' => '/value/',
    +      '{/var,undef}' => '/value',
    +      '{/var,x}/here' => '/value/1024/here',
    +      '{/var:1,var}' => '/v/value',
    +      '{/list}' => '/red,green,blue',
    +      '{/list*}' => '/red/green/blue',
    +      '{/list*,path:4}' => '/red/green/blue/%2Ffoo',
    +      '{/keys}' => '/semi,%3B,dot,.,comma,%2C',
    +      '{/keys*}' => '/semi=%3B/dot=./comma=%2C',
    +    }
       end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern 'http://example.org{-prefix|/|parts}/'" do
    -    Addressable::Template.new(
    -      "http://example.org{-prefix|/|parts}/"
    -    ).extract(@uri).should == {
    -      "parts" => ["", "", "something", "", ""]
    +  context "path-style expansion (;)" do
    +    it_behaves_like 'expands', {
    +      '{;who}' => ';who=fred',
    +      '{;half}' => ';half=50%25',
    +      '{;empty}' => ';empty',
    +      '{;v,empty,who}' => ';v=6;empty;who=fred',
    +      '{;v,bar,who}' => ';v=6;who=fred',
    +      '{;x,y}' => ';x=1024;y=768',
    +      '{;x,y,empty}' => ';x=1024;y=768;empty',
    +      '{;x,y,undef}' => ';x=1024;y=768',
    +      '{;hello:5}' => ';hello=Hello',
    +      '{;list}' => ';list=red,green,blue',
    +      '{;list*}' => ';list=red;list=green;list=blue',
    +      '{;keys}' => ';keys=semi,%3B,dot,.,comma,%2C',
    +      '{;keys*}' => ';semi=%3B;dot=.;comma=%2C',
         }
       end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern 'http://example.org/{-suffix|/|parts}'" do
    -    Addressable::Template.new(
    -      "http://example.org/{-suffix|/|parts}"
    -    ).extract(@uri).should == {
    -      "parts" => ["", "", "something", "", ""]
    +  context "form query expansion (?)" do
    +    it_behaves_like 'expands', {
    +      '{?who}' => '?who=fred',
    +      '{?half}' => '?half=50%25',
    +      '{?x,y}' => '?x=1024&y=768',
    +      '{?x,y,empty}' => '?x=1024&y=768&empty=',
    +      '{?x,y,undef}' => '?x=1024&y=768',
    +      '{?var:3}' => '?var=val',
    +      '{?list}' => '?list=red,green,blue',
    +      '{?list*}' => '?list=red&list=green&list=blue',
    +      '{?keys}' => '?keys=semi,%3B,dot,.,comma,%2C',
    +      '{?keys*}' => '?semi=%3B&dot=.&comma=%2C',
         }
       end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern 'http://example.org/{-list|/|parts}'" do
    -    Addressable::Template.new(
    -      "http://example.org/{-list|/|parts}"
    -    ).extract(@uri).should == {
    -      "parts" => ["", "", "something", "", ""]
    +  context "form query expansion (&)" do
    +    it_behaves_like 'expands', {
    +      '{&who}' => '&who=fred',
    +      '{&half}' => '&half=50%25',
    +      '?fixed=yes{&x}' => '?fixed=yes&x=1024',
    +      '{&x,y,empty}' => '&x=1024&y=768&empty=',
    +      '{&x,y,undef}' => '&x=1024&y=768',
    +      '{&var:3}' => '&var=val',
    +      '{&list}' => '&list=red,green,blue',
    +      '{&list*}' => '&list=red&list=green&list=blue',
    +      '{&keys}' => '&keys=semi,%3B,dot,.,comma,%2C',
    +      '{&keys*}' => '&semi=%3B&dot=.&comma=%2C',
         }
       end
     end
     
    -describe Addressable::URI, "when parsed from " +
    -    "'http://example.com/one/spacer/two/'" do
    -  before do
    -    @uri = Addressable::URI.parse("http://example.com/one/spacer/two/")
    -  end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/{first}/spacer/{second}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{first}/spacer/{second}/"
    -    ).extract(@uri).should == {
    -      "first" => "one",
    -      "second" => "two"
    -    }
    +class ExampleTwoProcessor
    +  def self.restore(name, value)
    +    return value.gsub(/-/, " ") if name == "query"
    +    return value
       end
     
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com{-prefix|/|stuff}/'" do
    -    Addressable::Template.new(
    -      "http://example.com{-prefix|/|stuff}/"
    -    ).extract(@uri).should == {
    -      "stuff" => ["one", "spacer", "two"]
    -    }
    +  def self.match(name)
    +    return ".*?" if name == "first"
    +    return ".*"
       end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/o{-prefix|/|stuff}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/o{-prefix|/|stuff}/"
    -    ).extract(@uri).should == nil
    +  def self.validate(name, value)
    +    return !!(value =~ /^[\w ]+$/) if name == "query"
    +    return true
       end
     
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/{first}/spacer{-prefix|/|stuff}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{first}/spacer{-prefix|/|stuff}/"
    -    ).extract(@uri).should == {
    -      "first" => "one",
    -      "stuff" => "two"
    -    }
    +  def self.transform(name, value)
    +    return value.gsub(/ /, "+") if name == "query"
    +    return value
       end
    +end
     
    -  it "should not match anything when extracting values " +
    -      "using the incorrect suffix pattern " +
    -      "'http://example.com/{-prefix|/|stuff}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{-prefix|/|stuff}/"
    -    ).extract(@uri).should == nil
    -  end
     
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com{-prefix|/|rot13}/' with the ExampleProcessor" do
    -    Addressable::Template.new(
    -      "http://example.com{-prefix|/|rot13}/"
    -    ).extract(@uri, ExampleProcessor).should == {
    -      "rot13" => ["bar", "fcnpre", "gjb"]
    +describe Addressable::Template do
    +  describe "Matching" do
    +    let(:uri){
    +      Addressable::URI.parse(
    +        "http://example.com/search/an-example-search-query/"
    +      )
         }
    -  end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com{-prefix|/|rot13}' with the ExampleProcessor" do
    -    Addressable::Template.new(
    -      "http://example.com{-prefix|/|rot13}"
    -    ).extract(@uri, ExampleProcessor).should == {
    -      "rot13" => ["bar", "fcnpre", "gjb", ""]
    +    let(:uri2){
    +      Addressable::URI.parse("http://example.com/a/b/c/")
         }
    -  end
    +    let(:uri3){
    +      Addressable::URI.parse("http://example.com/;a=1;b=2;c=3;first=foo")
    +    }
    +    let(:uri4){
    +      Addressable::URI.parse("http://example.com/?a=1&b=2&c=3&first=foo")
    +    }
    +    context "first uri with ExampleTwoProcessor" do
    +      subject{
    +        match = Addressable::Template.new(
    +          "http://example.com/search/{query}/"
    +        ).match(uri, ExampleTwoProcessor)
    +      }
    +      its(:variables){ should == ["query"]}
    +      its(:captures){ should == ["an example search query"]}
    +    end
     
    -  it "should not match anything when extracting values " +
    -      "using the incorrect suffix pattern " +
    -      "'http://example.com/{-prefix|/|rot13}' with the ExampleProcessor" do
    -    Addressable::Template.new(
    -      "http://example.com/{-prefix|/|rot13}"
    -    ).extract(@uri, ExampleProcessor).should == nil
    +    context "second uri with ExampleTwoProcessor" do
    +      subject{
    +        match = Addressable::Template.new(
    +          "http://example.com/{first}/{+second}/"
    +        ).match(uri2, ExampleTwoProcessor)
    +      }
    +      its(:variables){ should == ["first", "second"]}
    +      its(:captures){ should == ["a", "b/c"] }
    +    end
    +    context "second uri" do
    +      subject{
    +        match = Addressable::Template.new(
    +          "http://example.com/{first}{/second*}/"
    +        ).match(uri2)
    +      }
    +      its(:variables){ should == ["first", "second"]}
    +      its(:captures){ should == ["a", ["b","c"]] }
    +    end
    +    context "third uri" do
    +      subject{
    +        match = Addressable::Template.new(
    +          "http://example.com/{;hash*,first}"
    +        ).match(uri3)
    +      }
    +      its(:variables){ should == ["hash", "first"]}
    +      its(:captures){ should == [
    +        {"a" => "1", "b" => "2", "c" => "3", "first" => "foo"}, nil] }
    +    end
    +    context "fourth uri" do
    +      subject{
    +        match = Addressable::Template.new(
    +          "http://example.com/{?hash*,first}"
    +        ).match(uri4)
    +      }
    +      its(:variables){ should == ["hash", "first"]}
    +      its(:captures){ should == [
    +        {"a" => "1", "b" => "2", "c" => "3", "first"=> "foo"}, nil] }
    +    end
       end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/{-suffix|/|stuff}'" do
    -    Addressable::Template.new(
    -      "http://example.com/{-suffix|/|stuff}"
    -    ).extract(@uri).should == {
    -      "stuff" => ["one", "spacer", "two"]
    +  describe "extract" do
    +    let(:template) {
    +      Addressable::Template.new(
    +        "http://{host}{/segments*}/{?one,two,bogus}{#fragment}"
    +      )
         }
    +    let(:uri){ "http://example.com/a/b/c/?one=1&two=2#foo" }
    +    it "should be able to extract" do
    +      template.extract(uri).should == {
    +        "host" => "example.com",
    +        "segments" => %w(a b c),
    +        "one" => "1",
    +        "bogus" => nil,
    +        "two" => "2",
    +        "fragment" => "foo"
    +      }
    +    end
       end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/{-suffix|/|stuff}o'" do
    -    Addressable::Template.new(
    -      "http://example.com/{-suffix|/|stuff}o"
    -    ).extract(@uri).should == nil
    +  describe "Partial expand" do
    +    context "partial_expand with two simple values" do
    +      subject{
    +        Addressable::Template.new("http://example.com/{one}/{two}/")
    +      }
    +      it "builds a new pattern" do
    +        subject.partial_expand("one" => "1").pattern.should ==
    +          "http://example.com/1/{two}/"
    +      end
    +    end
    +    context "partial_expand query with missing param in middle" do
    +      subject{
    +        Addressable::Template.new("http://example.com/{?one,two,three}/")
    +      }
    +      it "builds a new pattern" do
    +        subject.partial_expand("one" => "1", "three" => "3").pattern.should ==
    +          "http://example.com/?one=1{&two}&three=3/"
    +      end
    +    end
    +    context "partial_expand with query string" do
    +      subject{
    +        Addressable::Template.new("http://example.com/{?two,one}/")
    +      }
    +      it "builds a new pattern" do
    +        subject.partial_expand("one" => "1").pattern.should ==
    +          "http://example.com/{?two}&one=1/"
    +      end
    +    end
    +    context "partial_expand with path operator" do
    +      subject{
    +        Addressable::Template.new("http://example.com{/one,two}/")
    +      }
    +      it "builds a new pattern" do
    +        subject.partial_expand("one" => "1").pattern.should ==
    +          "http://example.com/1{/two}/"
    +      end
    +    end
       end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/o{-suffix|/|stuff}'" do
    -    Addressable::Template.new(
    -      "http://example.com/o{-suffix|/|stuff}"
    -    ).extract(@uri).should == {"stuff"=>["ne", "spacer", "two"]}
    +  describe "Expand" do
    +    context "expand with a processor" do
    +      subject{
    +        Addressable::Template.new("http://example.com/search/{query}/")
    +      }
    +      it "processes spaces" do
    +        subject.expand({"query" => "an example search query"},
    +                      ExampleTwoProcessor).to_str.should ==
    +          "http://example.com/search/an+example+search+query/"
    +      end
    +      it "validates" do
    +        lambda{
    +          subject.expand({"query" => "Bogus!"},
    +                      ExampleTwoProcessor).to_str
    +        }.should raise_error(Addressable::Template::InvalidTemplateValueError)
    +      end
    +    end
    +    context "partial_expand query with missing param in middle" do
    +      subject{
    +        Addressable::Template.new("http://example.com/{?one,two,three}/")
    +      }
    +      it "builds a new pattern" do
    +        subject.partial_expand("one" => "1", "three" => "3").pattern.should ==
    +          "http://example.com/?one=1{&two}&three=3/"
    +      end
    +    end
    +    context "partial_expand with query string" do
    +      subject{
    +        Addressable::Template.new("http://example.com/{?two,one}/")
    +      }
    +      it "builds a new pattern" do
    +        subject.partial_expand("one" => "1").pattern.should ==
    +          "http://example.com/{?two}&one=1/"
    +      end
    +    end
    +    context "partial_expand with path operator" do
    +      subject{
    +        Addressable::Template.new("http://example.com{/one,two}/")
    +      }
    +      it "builds a new pattern" do
    +        subject.partial_expand("one" => "1").pattern.should ==
    +          "http://example.com/1{/two}/"
    +      end
    +    end
       end
    +  context "Matching with operators" do
    +    describe "Level 1:" do
    +      subject { Addressable::Template.new("foo{foo}/{bar}baz") }
    +      it "can match" do
    +        data = subject.match("foofoo/bananabaz")
    +        data.mapping["foo"].should == "foo"
    +        data.mapping["bar"].should == "banana"
    +      end
    +      it "lists vars" do
    +        subject.variables.should == ["foo", "bar"]
    +      end
    +    end
     
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/{first}/spacer/{-suffix|/|stuff}'" do
    -    Addressable::Template.new(
    -      "http://example.com/{first}/spacer/{-suffix|/|stuff}"
    -    ).extract(@uri).should == {
    -      "first" => "one",
    -      "stuff" => "two"
    -    }
    -  end
    +    describe "Level 2:" do
    +      subject { Addressable::Template.new("foo{+foo}{#bar}baz") }
    +      it "can match" do
    +        data = subject.match("foo/test/banana#bazbaz")
    +        data.mapping["foo"].should == "/test/banana"
    +        data.mapping["bar"].should == "baz"
    +      end
    +      it "lists vars" do
    +        subject.variables.should == ["foo", "bar"]
    +      end
    +    end
     
    -  it "should not match anything when extracting values " +
    -      "using the incorrect suffix pattern " +
    -      "'http://example.com/{-suffix|/|stuff}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{-suffix|/|stuff}/"
    -    ).extract(@uri).should == nil
    +    describe "Level 3:" do
    +      context "no operator" do
    +        subject { Addressable::Template.new("foo{foo,bar}baz") }
    +        it "can match" do
    +          data = subject.match("foofoo,barbaz")
    +          data.mapping["foo"].should == "foo"
    +          data.mapping["bar"].should == "bar"
    +        end
    +        it "lists vars" do
    +          subject.variables.should == ["foo", "bar"]
    +        end
    +      end
    +      context "+ operator" do
    +        subject { Addressable::Template.new("foo{+foo,bar}baz") }
    +        it "can match" do
    +          data = subject.match("foofoo/bar,barbaz")
    +          data.mapping["bar"].should == "foo/bar,bar"
    +          data.mapping["foo"].should == ""
    +        end
    +        it "lists vars" do
    +          subject.variables.should == ["foo", "bar"]
    +        end
    +      end
    +      context ". operator" do
    +        subject { Addressable::Template.new("foo{.foo,bar}baz") }
    +        it "can match" do
    +          data = subject.match("foo.foo.barbaz")
    +          data.mapping["foo"].should == "foo"
    +          data.mapping["bar"].should == "bar"
    +        end
    +        it "lists vars" do
    +          subject.variables.should == ["foo", "bar"]
    +        end
    +      end
    +      context "/ operator" do
    +        subject { Addressable::Template.new("foo{/foo,bar}baz") }
    +        it "can match" do
    +          data = subject.match("foo/foo/barbaz")
    +          data.mapping["foo"].should == "foo"
    +          data.mapping["bar"].should == "bar"
    +        end
    +        it "lists vars" do
    +          subject.variables.should == ["foo", "bar"]
    +        end
    +      end
    +      context "; operator" do
    +        subject { Addressable::Template.new("foo{;foo,bar,baz}baz") }
    +        it "can match" do
    +          data = subject.match("foo;foo=bar%20baz;bar=foo;bazbaz")
    +          data.mapping["foo"].should == "bar baz"
    +          data.mapping["bar"].should == "foo"
    +          data.mapping["baz"].should == ""
    +        end
    +        it "lists vars" do
    +          subject.variables.should == %w(foo bar baz)
    +        end
    +      end
    +      context "? operator" do
    +        subject { Addressable::Template.new("foo{?foo,bar}baz") }
    +        it "can match" do
    +          data = subject.match("foo?foo=bar%20baz&bar=foobaz")
    +          data.mapping["foo"].should == "bar baz"
    +          data.mapping["bar"].should == "foo"
    +        end
    +        it "lists vars" do
    +          subject.variables.should == %w(foo bar)
    +        end
    +      end
    +      context "& operator" do
    +        subject { Addressable::Template.new("foo{&foo,bar}baz") }
    +        it "can match" do
    +          data = subject.match("foo&foo=bar%20baz&bar=foobaz")
    +          data.mapping["foo"].should == "bar baz"
    +          data.mapping["bar"].should == "foo"
    +        end
    +        it "lists vars" do
    +          subject.variables.should == %w(foo bar)
    +        end
    +      end
    +    end
       end
     
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com/{-suffix|/|rot13}' with the ExampleProcessor" do
    -    Addressable::Template.new(
    -      "http://example.com/{-suffix|/|rot13}"
    -    ).extract(@uri, ExampleProcessor).should == {
    -      "rot13" => ["bar", "fcnpre", "gjb"]
    -    }
    -  end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://example.com{-suffix|/|rot13}' with the ExampleProcessor" do
    -    Addressable::Template.new(
    -      "http://example.com{-suffix|/|rot13}"
    -    ).extract(@uri, ExampleProcessor).should == {
    -      "rot13" => ["", "bar", "fcnpre", "gjb"]
    -    }
    -  end
    -
    -  it "should not match anything when extracting values " +
    -      "using the incorrect suffix pattern " +
    -      "'http://example.com/{-suffix|/|rot13}/' with the ExampleProcessor" do
    -    Addressable::Template.new(
    -      "http://example.com/{-suffix|/|rot13}/"
    -    ).extract(@uri, ExampleProcessor).should == nil
    -  end
    -end
    -
    -describe Addressable::URI, "when parsed from " +
    -    "'http://example.com/?email=bob@sporkmonger.com'" do
    -  before do
    -    @uri = Addressable::URI.parse(
    -      "http://example.com/?email=bob@sporkmonger.com"
    -    )
    -  end
    -
    -  it "should not match anything when extracting values " +
    -      "using the incorrect opt pattern " +
    -      "'http://example.com/?email={-opt|bogus@bogus.com|test}'" do
    -    Addressable::Template.new(
    -      "http://example.com/?email={-opt|bogus@bogus.com|test}"
    -    ).extract(@uri).should == nil
    -  end
    -
    -  it "should not match anything when extracting values " +
    -      "using the incorrect neg pattern " +
    -      "'http://example.com/?email={-neg|bogus@bogus.com|test}'" do
    -    Addressable::Template.new(
    -      "http://example.com/?email={-neg|bogus@bogus.com|test}"
    -    ).extract(@uri).should == nil
    -  end
    -
    -  it "should indicate a match when extracting values " +
    -      "using the opt pattern " +
    -      "'http://example.com/?email={-opt|bob@sporkmonger.com|test}'" do
    -    Addressable::Template.new(
    -      "http://example.com/?email={-opt|bob@sporkmonger.com|test}"
    -    ).extract(@uri).should == {}
    -  end
    -
    -  it "should indicate a match when extracting values " +
    -      "using the neg pattern " +
    -      "'http://example.com/?email={-neg|bob@sporkmonger.com|test}'" do
    -    Addressable::Template.new(
    -      "http://example.com/?email={-neg|bob@sporkmonger.com|test}"
    -    ).extract(@uri).should == {}
    -  end
    -end
    -
    -describe Addressable::URI, "when parsed from " +
    -    "'http://example.com/?email='" do
    -  before do
    -    @uri = Addressable::URI.parse(
    -      "http://example.com/?email="
    -    )
    -  end
    -
    -  it "should indicate a match when extracting values " +
    -      "using the opt pattern " +
    -      "'http://example.com/?email={-opt|bob@sporkmonger.com|test}'" do
    -    Addressable::Template.new(
    -      "http://example.com/?email={-opt|bob@sporkmonger.com|test}"
    -    ).extract(@uri).should == {}
    -  end
    -
    -  it "should indicate a match when extracting values " +
    -      "using the neg pattern " +
    -      "'http://example.com/?email={-neg|bob@sporkmonger.com|test}'" do
    -    Addressable::Template.new(
    -      "http://example.com/?email={-neg|bob@sporkmonger.com|test}"
    -    ).extract(@uri).should == {}
    -  end
    -end
    -
    -describe Addressable::URI, "when parsed from " +
    -    "'http://example.com/a/b/c/?one=1&two=2#foo'" do
    -  before do
    -    @uri = Addressable::URI.parse(
    -      "http://example.com/a/b/c/?one=1&two=2#foo"
    -    )
    -  end
    -
    -  it "should have the correct mapping when extracting values " +
    -      "using the pattern " +
    -      "'http://{host}/{-suffix|/|segments}?{-join|&|one,two}\#{fragment}'" do
    -    Addressable::Template.new(
    -      "http://{host}/{-suffix|/|segments}?{-join|&|one,two}\#{fragment}"
    -    ).extract(@uri).should == {
    -      "host" => "example.com",
    -      "segments" => ["a", "b", "c"],
    -      "one" => "1",
    -      "two" => "2",
    -      "fragment" => "foo"
    -    }
    -  end
    -
    -  it "should not match when extracting values " +
    -      "using the pattern " +
    -      "'http://{host}/{-suffix|/|segments}?{-join|&|one}\#{fragment}'" do
    -    Addressable::Template.new(
    -      "http://{host}/{-suffix|/|segments}?{-join|&|one}\#{fragment}"
    -    ).extract(@uri).should == nil
    -  end
    -
    -  it "should not match when extracting values " +
    -      "using the pattern " +
    -      "'http://{host}/{-suffix|/|segments}?{-join|&|bogus}\#{fragment}'" do
    -    Addressable::Template.new(
    -      "http://{host}/{-suffix|/|segments}?{-join|&|bogus}\#{fragment}"
    -    ).extract(@uri).should == nil
    -  end
    -
    -  it "should not match when extracting values " +
    -      "using the pattern " +
    -      "'http://{host}/{-suffix|/|segments}?" +
    -      "{-join|&|one,bogus}\#{fragment}'" do
    -    Addressable::Template.new(
    -      "http://{host}/{-suffix|/|segments}?{-join|&|one,bogus}\#{fragment}"
    -    ).extract(@uri).should == nil
    -  end
    -
    -  it "should not match when extracting values " +
    -      "using the pattern " +
    -      "'http://{host}/{-suffix|/|segments}?" +
    -      "{-join|&|one,two,bogus}\#{fragment}'" do
    -    Addressable::Template.new(
    -      "http://{host}/{-suffix|/|segments}?{-join|&|one,two,bogus}\#{fragment}"
    -    ).extract(@uri).should == {
    -      "host" => "example.com",
    -      "segments" => ["a", "b", "c"],
    -      "one" => "1",
    -      "two" => "2",
    -      "fragment" => "foo"
    -    }
    -  end
    -end
    -
    -describe Addressable::URI, "when given a pattern with bogus operators" do
    -  before do
    -    @uri = Addressable::URI.parse("http://example.com/a/b/c/")
    -  end
    -
    -  it "should raise an InvalidTemplateOperatorError" do
    -    (lambda do
    -      Addressable::Template.new(
    -        "http://example.com/{-bogus|/|a,b,c}/"
    -      ).extract(@uri)
    -    end).should raise_error(
    -      Addressable::Template::InvalidTemplateOperatorError
    -    )
    -  end
    -
    -  it "should raise an InvalidTemplateOperatorError" do
    -    (lambda do
    -      Addressable::Template.new(
    -        "http://example.com{-prefix|/|a,b,c}/"
    -      ).extract(@uri)
    -    end).should raise_error(
    -      Addressable::Template::InvalidTemplateOperatorError
    -    )
    -  end
    -
    -  it "should raise an InvalidTemplateOperatorError" do
    -    (lambda do
    -      Addressable::Template.new(
    -        "http://example.com/{-suffix|/|a,b,c}"
    -      ).extract(@uri)
    -    end).should raise_error(
    -      Addressable::Template::InvalidTemplateOperatorError
    -    )
    -  end
    -
    -  it "should raise an InvalidTemplateOperatorError" do
    -    (lambda do
    -      Addressable::Template.new(
    -        "http://example.com/{-list|/|a,b,c}/"
    -      ).extract(@uri)
    -    end).should raise_error(
    -      Addressable::Template::InvalidTemplateOperatorError
    -    )
    -  end
    -end
    -
    -describe Addressable::URI, "when given a mapping that contains an Array" do
    -  before do
    -    @mapping = {"query" => "an example search query".split(" ")}
    -  end
    -
    -  it "should result in 'http://example.com/search/an+example+search+query/'" +
    -      " when used to expand 'http://example.com/search/{-list|+|query}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/search/{-list|+|query}/"
    -    ).expand(@mapping).to_str.should ==
    -      "http://example.com/search/an+example+search+query/"
    -  end
    -
    -  it "should result in 'http://example.com/search/an+example+search+query/'" +
    -      " when used to expand 'http://example.com/search/{-list|+|query}/'" +
    -      " with a NoOpProcessor" do
    -    Addressable::Template.new(
    -      "http://example.com/search/{-list|+|query}/"
    -    ).expand(@mapping, NoOpProcessor).to_str.should ==
    -      "http://example.com/search/an+example+search+query/"
    -  end
    -end
    -
    -describe Addressable::URI, "when given an empty mapping" do
    -  before do
    -    @mapping = {}
    -  end
    -
    -  it "should result in 'http://example.com/search/'" +
    -      " when used to expand 'http://example.com/search/{-list|+|query}'" do
    -    Addressable::Template.new(
    -      "http://example.com/search/{-list|+|query}"
    -    ).expand(@mapping).to_str.should == "http://example.com/search/"
    -  end
    -
    -  it "should result in 'http://example.com'" +
    -      " when used to expand 'http://example.com{-prefix|/|foo}'" do
    -    Addressable::Template.new(
    -      "http://example.com{-prefix|/|foo}"
    -    ).expand(@mapping).to_str.should == "http://example.com"
    -  end
    -
    -  it "should result in 'http://example.com'" +
    -      " when used to expand 'http://example.com{-suffix|/|foo}'" do
    -    Addressable::Template.new(
    -      "http://example.com{-suffix|/|foo}"
    -    ).expand(@mapping).to_str.should == "http://example.com"
    -  end
    -end
    -
    -describe Addressable::URI, "when given the template pattern " +
    -    "'http://example.com/search/{query}/' " +
    -    "to be processed with the ExampleProcessor" do
    -  before do
    -    @pattern = "http://example.com/search/{query}/"
    -  end
    -
    -  it "should expand to " +
    -      "'http://example.com/search/an+example+search+query/' " +
    -      "with a mapping of {\"query\" => \"an example search query\"} " do
    -    Addressable::Template.new(
    -      "http://example.com/search/{query}/"
    -    ).expand({
    -      "query" => "an example search query"
    -    }, ExampleProcessor).to_s.should ==
    -      "http://example.com/search/an+example+search+query/"
    -  end
    -
    -  it "should raise an error " +
    -      "with a mapping of {\"query\" => \"invalid!\"}" do
    -    (lambda do
    -      Addressable::Template.new(
    -        "http://example.com/search/{query}/"
    -      ).expand({"query" => "invalid!"}, ExampleProcessor).to_s
    -    end).should raise_error(Addressable::Template::InvalidTemplateValueError)
    -  end
    -end
    -
    -# Section 3.3.1 of the URI Template draft v 01
    -describe Addressable::URI, "when given the mapping supplied in " +
    -    "Section 3.3.1 of the URI Template draft v 01" do
    -  before do
    -    @mapping = {
    -      "a" => "fred",
    -      "b" => "barney",
    -      "c" => "cheeseburger",
    -      "d" => "one two three",
    -      "e" => "20% tricky",
    -      "f" => "",
    -      "20" => "this-is-spinal-tap",
    -      "scheme" => "https",
    -      "p" => "quote=to+be+or+not+to+be",
    -      "q" => "hullo#world"
    -    }
    -  end
    -
    -  it "should result in 'http://example.org/page1#fred' " +
    -      "when used to expand 'http://example.org/page1\#{a}'" do
    -    Addressable::Template.new(
    -      "http://example.org/page1\#{a}"
    -    ).expand(@mapping).to_s.should == "http://example.org/page1#fred"
    -  end
    -
    -  it "should result in 'http://example.org/fred/barney/' " +
    -      "when used to expand 'http://example.org/{a}/{b}/'" do
    -    Addressable::Template.new(
    -      "http://example.org/{a}/{b}/"
    -    ).expand(@mapping).to_s.should == "http://example.org/fred/barney/"
    -  end
    -
    -  it "should result in 'http://example.org/fredbarney/' " +
    -      "when used to expand 'http://example.org/{a}{b}/'" do
    -    Addressable::Template.new(
    -      "http://example.org/{a}{b}/"
    -    ).expand(@mapping).to_s.should == "http://example.org/fredbarney/"
    -  end
    -
    -  it "should result in " +
    -      "'http://example.com/order/cheeseburger/cheeseburger/cheeseburger/' " +
    -      "when used to expand 'http://example.com/order/{c}/{c}/{c}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/order/{c}/{c}/{c}/"
    -    ).expand(@mapping).to_s.should ==
    -      "http://example.com/order/cheeseburger/cheeseburger/cheeseburger/"
    -  end
    -
    -  it "should result in 'http://example.org/one%20two%20three' " +
    -      "when used to expand 'http://example.org/{d}'" do
    -    Addressable::Template.new(
    -      "http://example.org/{d}"
    -    ).expand(@mapping).to_s.should ==
    -      "http://example.org/one%20two%20three"
    -  end
    -
    -  it "should result in 'http://example.org/20%25%20tricky' " +
    -      "when used to expand 'http://example.org/{e}'" do
    -    Addressable::Template.new(
    -      "http://example.org/{e}"
    -    ).expand(@mapping).to_s.should ==
    -      "http://example.org/20%25%20tricky"
    -  end
    -
    -  it "should result in 'http://example.com//' " +
    -      "when used to expand 'http://example.com/{f}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{f}/"
    -    ).expand(@mapping).to_s.should ==
    -      "http://example.com//"
    -  end
    -
    -  it "should result in " +
    -      "'https://this-is-spinal-tap.example.org?date=&option=fred' " +
    -      "when used to expand " +
    -      "'{scheme}://{20}.example.org?date={wilma}&option={a}'" do
    -    Addressable::Template.new(
    -      "{scheme}://{20}.example.org?date={wilma}&option={a}"
    -    ).expand(@mapping).to_s.should ==
    -      "https://this-is-spinal-tap.example.org?date=&option=fred"
    -  end
    -
    -  # The v 01 draft conflicts with the v 03 draft here.
    -  # The Addressable implementation uses v 03.
    -  it "should result in " +
    -      "'http://example.org?quote%3Dto%2Bbe%2Bor%2Bnot%2Bto%2Bbe' " +
    -      "when used to expand 'http://example.org?{p}'" do
    -    Addressable::Template.new(
    -      "http://example.org?{p}"
    -    ).expand(@mapping).to_s.should ==
    -      "http://example.org?quote%3Dto%2Bbe%2Bor%2Bnot%2Bto%2Bbe"
    -  end
    -
    -  # The v 01 draft conflicts with the v 03 draft here.
    -  # The Addressable implementation uses v 03.
    -  it "should result in 'http://example.com/hullo%23world' " +
    -      "when used to expand 'http://example.com/{q}'" do
    -    Addressable::Template.new(
    -      "http://example.com/{q}"
    -    ).expand(@mapping).to_s.should == "http://example.com/hullo%23world"
    -  end
    -end
    -
    -# Section 4.5 of the URI Template draft v 03
    -describe Addressable::URI, "when given the mapping supplied in " +
    -    "Section 4.5 of the URI Template draft v 03" do
    -  before do
    -    @mapping = {
    -      "foo" => "ϓ",
    -      "bar" => "fred",
    -      "baz" => "10,20,30",
    -      "qux" => ["10","20","30"],
    -      "corge" => [],
    -      "grault" => "",
    -      "garply" => "a/b/c",
    -      "waldo" => "ben & jerrys",
    -      "fred" => ["fred", "", "wilma"],
    -      "plugh" => ["ẛ", "ṡ"],
    -      "1-a_b.c" => "200"
    -    }
    -  end
    -
    -  it "should result in 'http://example.org/?q=fred' " +
    -      "when used to expand 'http://example.org/?q={bar}'" do
    -    Addressable::Template.new(
    -      "http://example.org/?q={bar}"
    -    ).expand(@mapping).to_s.should == "http://example.org/?q=fred"
    -  end
    -
    -  it "should result in '/' " +
    -      "when used to expand '/{xyzzy}'" do
    -    Addressable::Template.new(
    -      "/{xyzzy}"
    -    ).expand(@mapping).to_s.should == "/"
    -  end
    -
    -  it "should result in " +
    -      "'http://example.org/?foo=%CE%8E&bar=fred&baz=10%2C20%2C30' " +
    -      "when used to expand " +
    -      "'http://example.org/?{-join|&|foo,bar,xyzzy,baz}'" do
    -    Addressable::Template.new(
    -      "http://example.org/?{-join|&|foo,bar,xyzzy,baz}"
    -    ).expand(@mapping).to_s.should ==
    -      "http://example.org/?foo=%CE%8E&bar=fred&baz=10%2C20%2C30"
    -  end
    -
    -  it "should result in 'http://example.org/?d=10,20,30' " +
    -      "when used to expand 'http://example.org/?d={-list|,|qux}'" do
    -    Addressable::Template.new(
    -      "http://example.org/?d={-list|,|qux}"
    -    ).expand(
    -      @mapping
    -    ).to_s.should == "http://example.org/?d=10,20,30"
    -  end
    -
    -  it "should result in 'http://example.org/?d=10&d=20&d=30' " +
    -      "when used to expand 'http://example.org/?d={-list|&d=|qux}'" do
    -    Addressable::Template.new(
    -      "http://example.org/?d={-list|&d=|qux}"
    -    ).expand(
    -      @mapping
    -    ).to_s.should == "http://example.org/?d=10&d=20&d=30"
    -  end
    -
    -  it "should result in 'http://example.org/fredfred/a%2Fb%2Fc' " +
    -      "when used to expand 'http://example.org/{bar}{bar}/{garply}'" do
    -    Addressable::Template.new(
    -      "http://example.org/{bar}{bar}/{garply}"
    -    ).expand(
    -      @mapping
    -    ).to_s.should == "http://example.org/fredfred/a%2Fb%2Fc"
    -  end
    -
    -  it "should result in 'http://example.org/fred/fred//wilma' " +
    -      "when used to expand 'http://example.org/{bar}{-prefix|/|fred}'" do
    -    Addressable::Template.new(
    -      "http://example.org/{bar}{-prefix|/|fred}"
    -    ).expand(
    -      @mapping
    -    ).to_s.should == "http://example.org/fred/fred//wilma"
    -  end
    -
    -  it "should result in ':%E1%B9%A1:%E1%B9%A1:' " +
    -      "when used to expand '{-neg|:|corge}{-suffix|:|plugh}'" do
    -    # Note: We need to check the path, because technically, this is an
    -    # invalid URI.
    -    Addressable::Template.new(
    -      "{-neg|:|corge}{-suffix|:|plugh}"
    -    ).expand(@mapping).path.should == ":%E1%B9%A1:%E1%B9%A1:"
    -  end
    -
    -  it "should result in '../ben%20%26%20jerrys/' " +
    -      "when used to expand '../{waldo}/'" do
    -    Addressable::Template.new(
    -      "../{waldo}/"
    -    ).expand(
    -      @mapping
    -    ).to_s.should == "../ben%20%26%20jerrys/"
    -  end
    -
    -  it "should result in 'telnet:192.0.2.16:80' " +
    -      "when used to expand 'telnet:192.0.2.16{-opt|:80|grault}'" do
    -    Addressable::Template.new(
    -      "telnet:192.0.2.16{-opt|:80|grault}"
    -    ).expand(
    -      @mapping
    -    ).to_s.should == "telnet:192.0.2.16:80"
    -  end
    -
    -  it "should result in ':200:' " +
    -      "when used to expand ':{1-a_b.c}:'" do
    -    # Note: We need to check the path, because technically, this is an
    -    # invalid URI.
    -    Addressable::Template.new(
    -      ":{1-a_b.c}:"
    -    ).expand(@mapping).path.should == ":200:"
    -  end
    -end
    -
    -describe Addressable::URI, "when given a mapping that contains a " +
    -  "template-var within a value" do
    -  before do
    -    @mapping = {
    -      "a" => "{b}",
    -      "b" => "barney",
    -    }
    -  end
    -
    -  it "should result in 'http://example.com/%7Bb%7D/barney/' " +
    -      "when used to expand 'http://example.com/{a}/{b}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{a}/{b}/"
    -    ).expand(
    -      @mapping
    -    ).to_s.should == "http://example.com/%7Bb%7D/barney/"
    -  end
    -
    -  it "should result in 'http://example.com//%7Bb%7D/' " +
    -      "when used to expand 'http://example.com/{-opt|foo|foo}/{a}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{-opt|foo|foo}/{a}/"
    -    ).expand(
    -      @mapping
    -    ).to_s.should == "http://example.com//%7Bb%7D/"
    -  end
    -
    -  it "should result in 'http://example.com//%7Bb%7D/' " +
    -      "when used to expand 'http://example.com/{-neg|foo|b}/{a}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{-neg|foo|b}/{a}/"
    -    ).expand(
    -      @mapping
    -    ).to_s.should == "http://example.com//%7Bb%7D/"
    -  end
    -
    -  it "should result in 'http://example.com//barney/%7Bb%7D/' " +
    -      "when used to expand 'http://example.com/{-prefix|/|b}/{a}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{-prefix|/|b}/{a}/"
    -    ).expand(
    -      @mapping
    -    ).to_s.should == "http://example.com//barney/%7Bb%7D/"
    -  end
    -
    -  it "should result in 'http://example.com/barney//%7Bb%7D/' " +
    -      "when used to expand 'http://example.com/{-suffix|/|b}/{a}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{-suffix|/|b}/{a}/"
    -    ).expand(
    -      @mapping
    -    ).to_s.should == "http://example.com/barney//%7Bb%7D/"
    -  end
    -
    -  it "should result in 'http://example.com/%7Bb%7D/?b=barney&c=42' " +
    -      "when used to expand 'http://example.com/{a}/?{-join|&|b,c=42}'" do
    -    Addressable::Template.new(
    -      "http://example.com/{a}/?{-join|&|b,c=42}"
    -    ).expand(
    -      @mapping
    -    ).to_s.should == "http://example.com/%7Bb%7D/?b=barney&c=42"
    -  end
    -
    -  it "should result in 'http://example.com/42/?b=barney' " +
    -      "when used to expand 'http://example.com/{c=42}/?{-join|&|b}'" do
    -    Addressable::Template.new(
    -      "http://example.com/{c=42}/?{-join|&|b}"
    -    ).expand(@mapping).to_s.should == "http://example.com/42/?b=barney"
    -  end
    -end
    -
    -describe Addressable::URI, "when given a single variable mapping" do
    -  before do
    -    @mapping = {
    -      "foo" => "fred"
    -    }
    -  end
    -
    -  it "should result in 'fred' when used to expand '{foo}'" do
    -    Addressable::Template.new(
    -      "{foo}"
    -    ).expand(@mapping).to_s.should == "fred"
    -  end
    -
    -  it "should result in 'wilma' when used to expand '{bar=wilma}'" do
    -    Addressable::Template.new(
    -      "{bar=wilma}"
    -    ).expand(@mapping).to_s.should == "wilma"
    -  end
    -
    -  it "should result in '' when used to expand '{baz}'" do
    -    Addressable::Template.new(
    -      "{baz}"
    -    ).expand(@mapping).to_s.should == ""
    -  end
    -end
    -
    -describe Addressable::URI, "when given a simple mapping" do
    -  before do
    -    @mapping = {
    -      "foo" => "fred",
    -      "bar" => "barney",
    -      "baz" => ""
    -    }
    -  end
    -
    -  it "should result in 'foo=fred&bar=barney&baz=' when used to expand " +
    -      "'{-join|&|foo,bar,baz,qux}'" do
    -    Addressable::Template.new(
    -      "{-join|&|foo,bar,baz,qux}"
    -    ).expand(@mapping).to_s.should == "foo=fred&bar=barney&baz="
    -  end
    -
    -  it "should result in 'bar=barney' when used to expand " +
    -      "'{-join|&|bar}'" do
    -    Addressable::Template.new(
    -      "{-join|&|bar}"
    -    ).expand(@mapping).to_s.should == "bar=barney"
    -  end
    -
    -  it "should result in '' when used to expand " +
    -      "'{-join|&|qux}'" do
    -    Addressable::Template.new(
    -      "{-join|&|qux}"
    -    ).expand(@mapping).to_s.should == ""
    -  end
    -end
    -
    -describe Addressable::URI, "extracting defaults from a pattern" do
    -  before do
    -    @template = Addressable::Template.new("{foo}{bar=baz}{-opt|found|cond}")
    -  end
    -
    -  it "should extract default value" do
    -    @template.variable_defaults.should == {"bar" => "baz"}
    -  end
    -end
    -
    -describe Addressable::URI, "when given a mapping with symbol keys" do
    -  before do
    -    @mapping = { :name => "fred" }
    -  end
    -
    -  it "should result in 'fred' when used to expand '{foo}'" do
    -    Addressable::Template.new(
    -      "{name}"
    -    ).expand(@mapping).to_s.should == "fred"
    -  end
    -end
    -
    -describe Addressable::URI, "when given a mapping with bogus keys" do
    -  before do
    -    @mapping = { Object.new => "fred" }
    -  end
    -
    -  it "should raise an error" do
    -    (lambda do
    -      Addressable::Template.new(
    -        "{name}"
    -      ).expand(@mapping)
    -    end).should raise_error(TypeError)
    -  end
    -end
    -
    -describe Addressable::URI, "when given a mapping with numeric values" do
    -  before do
    -    @mapping = { :id => 123 }
    -  end
    -
    -  it "should result in 'fred' when used to expand '{foo}'" do
    -    Addressable::Template.new(
    -      "{id}"
    -    ).expand(@mapping).to_s.should == "123"
    -  end
    -end
    -
    -describe Addressable::URI, "when given a mapping containing values " +
    -    "that are already percent-encoded" do
    -  before do
    -    @mapping = {
    -      "a" => "%7Bb%7D"
    -    }
    -  end
    -
    -  it "should result in 'http://example.com/%257Bb%257D/' " +
    -      "when used to expand 'http://example.com/{a}/'" do
    -    Addressable::Template.new(
    -      "http://example.com/{a}/"
    -    ).expand(@mapping).to_s.should == "http://example.com/%257Bb%257D/"
    -  end
    -end
    -
    -describe Addressable::URI, "when given a pattern with bogus operators" do
    -  it "should raise an InvalidTemplateOperatorError" do
    -    (lambda do
    -      Addressable::Template.new(
    -        "http://example.com/{-bogus|/|a,b,c}/"
    -      ).expand({
    -        "a" => "a", "b" => "b", "c" => "c"
    -      })
    -    end).should raise_error(
    -      Addressable::Template::InvalidTemplateOperatorError
    -    )
    -  end
    -
    -  it "should raise an InvalidTemplateOperatorError" do
    -    (lambda do
    -      Addressable::Template.new(
    -        "http://example.com/{-prefix|/|a,b,c}/"
    -      ).expand({
    -        "a" => "a", "b" => "b", "c" => "c"
    -      })
    -    end).should raise_error(
    -      Addressable::Template::InvalidTemplateOperatorError
    -    )
    -  end
    -
    -  it "should raise an InvalidTemplateOperatorError" do
    -    (lambda do
    -      Addressable::Template.new(
    -        "http://example.com/{-suffix|/|a,b,c}/"
    -      ).expand({
    -        "a" => "a", "b" => "b", "c" => "c"
    -      })
    -    end).should raise_error(
    -      Addressable::Template::InvalidTemplateOperatorError
    -    )
    -  end
    -
    -  it "should raise an InvalidTemplateOperatorError" do
    -    (lambda do
    -      Addressable::Template.new(
    -        "http://example.com/{-jo
    ... [truncated]
    
  • spec/addressable/uri_template_spec.rb+0 758 removed
    @@ -1,758 +0,0 @@
    -require "addressable/uri_template"
    -
    -shared_examples_for 'expands' do |tests|
    -  tests.each do |template, expansion|
    -    it "#{template} to #{expansion}" do
    -      tmpl = Addressable::UriTemplate.new(template).expand(subject)
    -      tmpl.to_str.should == expansion
    -    end
    -  end
    -end
    -
    -describe "Level 1:" do
    -  subject{
    -    {:var => "value", :hello => "Hello World!"}
    -  }
    -  it_behaves_like 'expands', {
    -    '{var}' => 'value',
    -    '{hello}' => 'Hello%20World%21'
    -  }
    -end
    -
    -describe "Level 2" do
    -  subject{
    -    {
    -      :var => "value",
    -      :hello => "Hello World!",
    -      :path => "/foo/bar"
    -    }
    -  }
    -  context "Operator +:" do
    -    it_behaves_like 'expands', {
    -      '{+var}' => 'value',
    -      '{+hello}' => 'Hello%20World!',
    -      '{+path}/here' => '/foo/bar/here',
    -      'here?ref={+path}' => 'here?ref=/foo/bar'
    -    }
    -  end
    -  context "Operator #:" do
    -    it_behaves_like 'expands', {
    -      'X{#var}' => 'X#value',
    -      'X{#hello}' => 'X#Hello%20World!',
    -    }
    -  end
    -end
    -
    -describe "Level 3" do
    -  subject{
    -    {
    -      :var => "value",
    -      :hello => "Hello World!",
    -      :empty => "",
    -      :path => "/foo/bar",
    -      :x => "1024",
    -      :y => "768"
    -    }
    -  }
    -  context "Operator nil (multiple vars):" do
    -    it_behaves_like 'expands', {
    -      'map?{x,y}' => 'map?1024,768',
    -      '{x,hello,y}' => '1024,Hello%20World%21,768'
    -    }
    -  end
    -  context "Operator + (multiple vars):" do
    -    it_behaves_like 'expands', {
    -      '{+x,hello,y}' => '1024,Hello%20World!,768',
    -      '{+path,x}/here' => '/foo/bar,1024/here',
    -    }
    -  end
    -  context "Operator # (multiple vars):" do
    -    it_behaves_like 'expands', {
    -      '{#x,hello,y}' => '#1024,Hello%20World!,768',
    -      '{#path,x}/here' => '#/foo/bar,1024/here',
    -    }
    -  end
    -  context "Operator ." do
    -    it_behaves_like 'expands', {
    -      'X{.var}' => 'X.value',
    -      'X{.x,y}' => 'X.1024.768',
    -    }
    -  end
    -  context "Operator /" do
    -    it_behaves_like 'expands', {
    -      '{/var}' => '/value',
    -      '{/var,x}/here' => '/value/1024/here',
    -    }
    -  end
    -  context "Operator ;" do
    -    it_behaves_like 'expands', {
    -      '{;x,y}' => ';x=1024;y=768',
    -      '{;x,y,empty}' => ';x=1024;y=768;empty',
    -    }
    -  end
    -  context "Operator ?" do
    -    it_behaves_like 'expands', {
    -      '{?x,y}' => '?x=1024&y=768',
    -      '{?x,y,empty}' => '?x=1024&y=768&empty=',
    -    }
    -  end
    -  context "Operator &" do
    -    it_behaves_like 'expands', {
    -      '?fixed=yes{&x}' => '?fixed=yes&x=1024',
    -      '{&x,y,empty}' => '&x=1024&y=768&empty=',
    -    }
    -  end
    -end
    -describe "Level 4" do
    -  subject{
    -    {
    -      :var => "value",
    -      :hello => "Hello World!",
    -      :path => "/foo/bar",
    -      :semi => ";",
    -      :list => %w(red green blue),
    -      :keys => {"semi" => ';', "dot" => '.', "comma" => ','}
    -    }
    -  }
    -  context "Expansion with value modifiers" do
    -    it_behaves_like 'expands', {
    -      '{var:3}' => 'val',
    -      '{var:30}' => 'value',
    -      '{list}' => 'red,green,blue',
    -      '{list*}' => 'red,green,blue',
    -      '{keys}' => 'semi,%3B,dot,.,comma,%2C',
    -      '{keys*}' => 'semi=%3B,dot=.,comma=%2C',
    -    }
    -  end
    -  context "Operator + with value modifiers" do
    -    it_behaves_like 'expands', {
    -      '{+path:6}/here' => '/foo/b/here',
    -      '{+list}' => 'red,green,blue',
    -      '{+list*}' => 'red,green,blue',
    -      '{+keys}' => 'semi,;,dot,.,comma,,',
    -      '{+keys*}' => 'semi=;,dot=.,comma=,',
    -    }
    -  end
    -  context "Operator # with value modifiers" do
    -    it_behaves_like 'expands', {
    -      '{#path:6}/here' => '#/foo/b/here',
    -      '{#list}' => '#red,green,blue',
    -      '{#list*}' => '#red,green,blue',
    -      '{#keys}' => '#semi,;,dot,.,comma,,',
    -      '{#keys*}' => '#semi=;,dot=.,comma=,',
    -    }
    -  end
    -  context "Operator . with value modifiers" do
    -    it_behaves_like 'expands', {
    -      'X{.var:3}' => 'X.val',
    -      'X{.list}' => 'X.red,green,blue',
    -      'X{.list*}' => 'X.red.green.blue',
    -      'X{.keys}' => 'X.semi,%3B,dot,.,comma,%2C',
    -      'X{.keys*}' => 'X.semi=%3B.dot=..comma=%2C',
    -    }
    -  end
    -  context "Operator / with value modifiers" do
    -    it_behaves_like 'expands', {
    -      '{/var:1,var}' => '/v/value',
    -      '{/list}' => '/red,green,blue',
    -      '{/list*}' => '/red/green/blue',
    -      '{/list*,path:4}' => '/red/green/blue/%2Ffoo',
    -      '{/keys}' => '/semi,%3B,dot,.,comma,%2C',
    -      '{/keys*}' => '/semi=%3B/dot=./comma=%2C',
    -    }
    -  end
    -  context "Operator ; with value modifiers" do
    -    it_behaves_like 'expands', {
    -      '{;hello:5}' => ';hello=Hello',
    -      '{;list}' => ';list=red,green,blue',
    -      '{;list*}' => ';list=red;list=green;list=blue',
    -      '{;keys}' => ';keys=semi,%3B,dot,.,comma,%2C',
    -      '{;keys*}' => ';semi=%3B;dot=.;comma=%2C',
    -    }
    -  end
    -  context "Operator ? with value modifiers" do
    -    it_behaves_like 'expands', {
    -      '{?var:3}' => '?var=val',
    -      '{?list}' => '?list=red,green,blue',
    -      '{?list*}' => '?list=red&list=green&list=blue',
    -      '{?keys}' => '?keys=semi,%3B,dot,.,comma,%2C',
    -      '{?keys*}' => '?semi=%3B&dot=.&comma=%2C',
    -    }
    -  end
    -  context "Operator & with value modifiers" do
    -    it_behaves_like 'expands', {
    -      '{&var:3}' => '&var=val',
    -      '{&list}' => '&list=red,green,blue',
    -      '{&list*}' => '&list=red&list=green&list=blue',
    -      '{&keys}' => '&keys=semi,%3B,dot,.,comma,%2C',
    -      '{&keys*}' => '&semi=%3B&dot=.&comma=%2C',
    -    }
    -  end
    -end
    -describe "Modifiers" do
    -  subject{
    -    {
    -      :var => "value",
    -      :semi => ";",
    -      :year => %w(1965 2000 2012),
    -      :dom => %w(example com)
    -    }
    -  }
    -  context "length" do
    -    it_behaves_like 'expands', {
    -      '{var:3}' => 'val',
    -      '{var:30}' => 'value',
    -      '{var}' => 'value',
    -      '{semi}' => '%3B',
    -      '{semi:2}' => '%3B',
    -    }
    -  end
    -  context "explode" do
    -    it_behaves_like 'expands', {
    -      'find{?year*}' => 'find?year=1965&year=2000&year=2012',
    -      'www{.dom*}' => 'www.example.com',
    -    }
    -  end
    -end
    -describe "Expansion" do
    -  subject{
    -    {
    -      :count => [ "one", "two", "three" ],
    -      :dom => [ "example", "com" ],
    -      :dub   => "me/too",
    -      :hello => "Hello World!",
    -      :half  => "50%",
    -      :var   => "value",
    -      :who   => "fred",
    -      :base  => "http://example.com/home/",
    -      :path  => "/foo/bar",
    -      :list  => [ "red", "green", "blue" ],
    -      :keys  => { "semi"=>";","dot"=>".","comma"=>"," },
    -      :v     => "6",
    -      :x     => "1024",
    -      :y     => "768",
    -      :empty => "",
    -      :empty_keys  => {},
    -      :undef => nil,
    -    }
    -  }
    -  context "concatenation" do
    -    it_behaves_like 'expands', {
    -      '{count}' => 'one,two,three',
    -      '{count*}' => 'one,two,three',
    -      '{/count}' => '/one,two,three',
    -      '{/count*}' => '/one/two/three',
    -      '{;count}' => ';count=one,two,three',
    -      '{;count*}' => ';count=one;count=two;count=three',
    -      '{?count}' => '?count=one,two,three',
    -      '{?count*}' => '?count=one&count=two&count=three',
    -      '{&count*}' => '&count=one&count=two&count=three',
    -    }
    -  end
    -  context "simple expansion" do
    -    it_behaves_like 'expands', {
    -      '{var}' => 'value',
    -      '{hello}' => 'Hello%20World%21',
    -      '{half}' => '50%25',
    -      'O{empty}X' => 'OX',
    -      'O{undef}X' => 'OX',
    -      '{x,y}' => '1024,768',
    -      '{x,hello,y}' => '1024,Hello%20World%21,768',
    -      '?{x,empty}' => '?1024,',
    -      '?{x,undef}' => '?1024',
    -      '?{undef,y}' => '?768',
    -      '{var:3}' => 'val',
    -      '{var:30}' => 'value',
    -      '{list}' => 'red,green,blue',
    -      '{list*}' => 'red,green,blue',
    -      '{keys}' => 'semi,%3B,dot,.,comma,%2C',
    -      '{keys*}' => 'semi=%3B,dot=.,comma=%2C',
    -    }
    -  end
    -  context "reserved expansion (+)" do
    -    it_behaves_like 'expands', {
    -      '{+var}' => 'value',
    -      '{+hello}' => 'Hello%20World!',
    -      '{+half}' => '50%25',
    -      '{base}index' => 'http%3A%2F%2Fexample.com%2Fhome%2Findex',
    -      '{+base}index' => 'http://example.com/home/index',
    -      'O{+empty}X' => 'OX',
    -      'O{+undef}X' => 'OX',
    -      '{+path}/here' => '/foo/bar/here',
    -      'here?ref={+path}' => 'here?ref=/foo/bar',
    -      'up{+path}{var}/here' => 'up/foo/barvalue/here',
    -      '{+x,hello,y}' => '1024,Hello%20World!,768',
    -      '{+path,x}/here' => '/foo/bar,1024/here',
    -      '{+path:6}/here' => '/foo/b/here',
    -      '{+list}' => 'red,green,blue',
    -      '{+list*}' => 'red,green,blue',
    -      '{+keys}' => 'semi,;,dot,.,comma,,',
    -      '{+keys*}' => 'semi=;,dot=.,comma=,',
    -    }
    -  end
    -  context "fragment expansion (#)" do
    -    it_behaves_like 'expands', {
    -      '{#var}' => '#value',
    -      '{#hello}' => '#Hello%20World!',
    -      '{#half}' => '#50%25',
    -      'foo{#empty}' => 'foo#',
    -      'foo{#undef}' => 'foo',
    -      '{#x,hello,y}' => '#1024,Hello%20World!,768',
    -      '{#path,x}/here' => '#/foo/bar,1024/here',
    -      '{#path:6}/here' => '#/foo/b/here',
    -      '{#list}' => '#red,green,blue',
    -      '{#list*}' => '#red,green,blue',
    -      '{#keys}' => '#semi,;,dot,.,comma,,',
    -      '{#keys*}' => '#semi=;,dot=.,comma=,',
    -    }
    -  end
    -  context "label expansion (.)" do
    -    it_behaves_like 'expands', {
    -      '{.who}' => '.fred',
    -      '{.who,who}' => '.fred.fred',
    -      '{.half,who}' => '.50%25.fred',
    -      'www{.dom*}' => 'www.example.com',
    -      'X{.var}' => 'X.value',
    -      'X{.empty}' => 'X.',
    -      'X{.undef}' => 'X',
    -      'X{.var:3}' => 'X.val',
    -      'X{.list}' => 'X.red,green,blue',
    -      'X{.list*}' => 'X.red.green.blue',
    -      'X{.keys}' => 'X.semi,%3B,dot,.,comma,%2C',
    -      'X{.keys*}' => 'X.semi=%3B.dot=..comma=%2C',
    -      'X{.empty_keys}' => 'X',
    -      'X{.empty_keys*}' => 'X',
    -    }
    -  end
    -  context "path expansion (/)" do
    -    it_behaves_like 'expands', {
    -      '{/who}' => '/fred',
    -      '{/who,who}' => '/fred/fred',
    -      '{/half,who}' => '/50%25/fred',
    -      '{/who,dub}' => '/fred/me%2Ftoo',
    -      '{/var}' => '/value',
    -      '{/var,empty}' => '/value/',
    -      '{/var,undef}' => '/value',
    -      '{/var,x}/here' => '/value/1024/here',
    -      '{/var:1,var}' => '/v/value',
    -      '{/list}' => '/red,green,blue',
    -      '{/list*}' => '/red/green/blue',
    -      '{/list*,path:4}' => '/red/green/blue/%2Ffoo',
    -      '{/keys}' => '/semi,%3B,dot,.,comma,%2C',
    -      '{/keys*}' => '/semi=%3B/dot=./comma=%2C',
    -    }
    -  end
    -  context "path-style expansion (;)" do
    -    it_behaves_like 'expands', {
    -      '{;who}' => ';who=fred',
    -      '{;half}' => ';half=50%25',
    -      '{;empty}' => ';empty',
    -      '{;v,empty,who}' => ';v=6;empty;who=fred',
    -      '{;v,bar,who}' => ';v=6;who=fred',
    -      '{;x,y}' => ';x=1024;y=768',
    -      '{;x,y,empty}' => ';x=1024;y=768;empty',
    -      '{;x,y,undef}' => ';x=1024;y=768',
    -      '{;hello:5}' => ';hello=Hello',
    -      '{;list}' => ';list=red,green,blue',
    -      '{;list*}' => ';list=red;list=green;list=blue',
    -      '{;keys}' => ';keys=semi,%3B,dot,.,comma,%2C',
    -      '{;keys*}' => ';semi=%3B;dot=.;comma=%2C',
    -    }
    -  end
    -  context "form query expansion (?)" do
    -    it_behaves_like 'expands', {
    -      '{?who}' => '?who=fred',
    -      '{?half}' => '?half=50%25',
    -      '{?x,y}' => '?x=1024&y=768',
    -      '{?x,y,empty}' => '?x=1024&y=768&empty=',
    -      '{?x,y,undef}' => '?x=1024&y=768',
    -      '{?var:3}' => '?var=val',
    -      '{?list}' => '?list=red,green,blue',
    -      '{?list*}' => '?list=red&list=green&list=blue',
    -      '{?keys}' => '?keys=semi,%3B,dot,.,comma,%2C',
    -      '{?keys*}' => '?semi=%3B&dot=.&comma=%2C',
    -    }
    -  end
    -  context "form query expansion (&)" do
    -    it_behaves_like 'expands', {
    -      '{&who}' => '&who=fred',
    -      '{&half}' => '&half=50%25',
    -      '?fixed=yes{&x}' => '?fixed=yes&x=1024',
    -      '{&x,y,empty}' => '&x=1024&y=768&empty=',
    -      '{&x,y,undef}' => '&x=1024&y=768',
    -      '{&var:3}' => '&var=val',
    -      '{&list}' => '&list=red,green,blue',
    -      '{&list*}' => '&list=red&list=green&list=blue',
    -      '{&keys}' => '&keys=semi,%3B,dot,.,comma,%2C',
    -      '{&keys*}' => '&semi=%3B&dot=.&comma=%2C',
    -    }
    -  end
    -end
    -
    -class ExampleTwoProcessor
    -  def self.restore(name, value)
    -    return value.gsub(/-/, " ") if name == "query"
    -    return value
    -  end
    -
    -  def self.match(name)
    -    return ".*?" if name == "first"
    -    return ".*"
    -  end
    -  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
    -
    -
    -describe Addressable::UriTemplate do
    -  describe "Matching" do
    -    let(:uri){
    -      Addressable::URI.parse(
    -        "http://example.com/search/an-example-search-query/"
    -      )
    -    }
    -    let(:uri2){
    -      Addressable::URI.parse("http://example.com/a/b/c/")
    -    }
    -    let(:uri3){
    -      Addressable::URI.parse("http://example.com/;a=1;b=2;c=3;first=foo")
    -    }
    -    let(:uri4){
    -      Addressable::URI.parse("http://example.com/?a=1&b=2&c=3&first=foo")
    -    }
    -    context "first uri with ExampleTwoProcessor" do
    -      subject{
    -        match = Addressable::UriTemplate.new(
    -          "http://example.com/search/{query}/"
    -        ).match(uri, ExampleTwoProcessor)
    -      }
    -      its(:variables){ should == ["query"]}
    -      its(:captures){ should == ["an example search query"]}
    -    end
    -
    -    context "second uri with ExampleTwoProcessor" do
    -      subject{
    -        match = Addressable::UriTemplate.new(
    -          "http://example.com/{first}/{+second}/"
    -        ).match(uri2, ExampleTwoProcessor)
    -      }
    -      its(:variables){ should == ["first", "second"]}
    -      its(:captures){ should == ["a", "b/c"] }
    -    end
    -    context "second uri" do
    -      subject{
    -        match = Addressable::UriTemplate.new(
    -          "http://example.com/{first}{/second*}/"
    -        ).match(uri2)
    -      }
    -      its(:variables){ should == ["first", "second"]}
    -      its(:captures){ should == ["a", ["b","c"]] }
    -    end
    -    context "third uri" do
    -      subject{
    -        match = Addressable::UriTemplate.new(
    -          "http://example.com/{;hash*,first}"
    -        ).match(uri3)
    -      }
    -      its(:variables){ should == ["hash", "first"]}
    -      its(:captures){ should == [
    -        {"a" => "1", "b" => "2", "c" => "3", "first" => "foo"}, nil] }
    -    end
    -    context "fourth uri" do
    -      subject{
    -        match = Addressable::UriTemplate.new(
    -          "http://example.com/{?hash*,first}"
    -        ).match(uri4)
    -      }
    -      its(:variables){ should == ["hash", "first"]}
    -      its(:captures){ should == [
    -        {"a" => "1", "b" => "2", "c" => "3", "first"=> "foo"}, nil] }
    -    end
    -  end
    -  describe "extract" do
    -    let(:template) {
    -      Addressable::UriTemplate.new(
    -        "http://{host}{/segments*}/{?one,two,bogus}{#fragment}"
    -      )
    -    }
    -    let(:uri){ "http://example.com/a/b/c/?one=1&two=2#foo" }
    -    it "should be able to extract" do
    -      template.extract(uri).should == {
    -        "host" => "example.com",
    -        "segments" => %w(a b c),
    -        "one" => "1",
    -        "bogus" => nil,
    -        "two" => "2",
    -        "fragment" => "foo"
    -      }
    -    end
    -  end
    -  describe "Partial expand" do
    -    context "partial_expand with two simple values" do
    -      subject{
    -        Addressable::UriTemplate.new("http://example.com/{one}/{two}/")
    -      }
    -      it "builds a new pattern" do
    -        subject.partial_expand("one" => "1").pattern.should ==
    -          "http://example.com/1/{two}/"
    -      end
    -    end
    -    context "partial_expand query with missing param in middle" do
    -      subject{
    -        Addressable::UriTemplate.new("http://example.com/{?one,two,three}/")
    -      }
    -      it "builds a new pattern" do
    -        subject.partial_expand("one" => "1", "three" => "3").pattern.should ==
    -          "http://example.com/?one=1{&two}&three=3/"
    -      end
    -    end
    -    context "partial_expand with query string" do
    -      subject{
    -        Addressable::UriTemplate.new("http://example.com/{?two,one}/")
    -      }
    -      it "builds a new pattern" do
    -        subject.partial_expand("one" => "1").pattern.should ==
    -          "http://example.com/{?two}&one=1/"
    -      end
    -    end
    -    context "partial_expand with path operator" do
    -      subject{
    -        Addressable::UriTemplate.new("http://example.com{/one,two}/")
    -      }
    -      it "builds a new pattern" do
    -        subject.partial_expand("one" => "1").pattern.should ==
    -          "http://example.com/1{/two}/"
    -      end
    -    end
    -  end
    -  describe "Expand" do
    -    context "expand with a processor" do
    -      subject{
    -        Addressable::UriTemplate.new("http://example.com/search/{query}/")
    -      }
    -      it "processes spaces" do
    -        subject.expand({"query" => "an example search query"},
    -                      ExampleTwoProcessor).to_str.should ==
    -          "http://example.com/search/an+example+search+query/"
    -      end
    -      it "validates" do
    -        lambda{
    -          subject.expand({"query" => "Bogus!"},
    -                      ExampleTwoProcessor).to_str
    -        }.should raise_error(Addressable::Template::InvalidTemplateValueError)
    -      end
    -    end
    -    context "partial_expand query with missing param in middle" do
    -      subject{
    -        Addressable::UriTemplate.new("http://example.com/{?one,two,three}/")
    -      }
    -      it "builds a new pattern" do
    -        subject.partial_expand("one" => "1", "three" => "3").pattern.should ==
    -          "http://example.com/?one=1{&two}&three=3/"
    -      end
    -    end
    -    context "partial_expand with query string" do
    -      subject{
    -        Addressable::UriTemplate.new("http://example.com/{?two,one}/")
    -      }
    -      it "builds a new pattern" do
    -        subject.partial_expand("one" => "1").pattern.should ==
    -          "http://example.com/{?two}&one=1/"
    -      end
    -    end
    -    context "partial_expand with path operator" do
    -      subject{
    -        Addressable::UriTemplate.new("http://example.com{/one,two}/")
    -      }
    -      it "builds a new pattern" do
    -        subject.partial_expand("one" => "1").pattern.should ==
    -          "http://example.com/1{/two}/"
    -      end
    -    end
    -  end
    -  context "Matching with operators" do
    -    describe "Level 1:" do
    -      subject { Addressable::UriTemplate.new("foo{foo}/{bar}baz") }
    -      it "can match" do
    -        data = subject.match("foofoo/bananabaz")
    -        data.mapping["foo"].should == "foo"
    -        data.mapping["bar"].should == "banana"
    -      end
    -      it "lists vars" do
    -        subject.variables.should == ["foo", "bar"]
    -      end
    -    end
    -
    -    describe "Level 2:" do
    -      subject { Addressable::UriTemplate.new("foo{+foo}{#bar}baz") }
    -      it "can match" do
    -        data = subject.match("foo/test/banana#bazbaz")
    -        data.mapping["foo"].should == "/test/banana"
    -        data.mapping["bar"].should == "baz"
    -      end
    -      it "lists vars" do
    -        subject.variables.should == ["foo", "bar"]
    -      end
    -    end
    -
    -    describe "Level 3:" do
    -      context "no operator" do
    -        subject { Addressable::UriTemplate.new("foo{foo,bar}baz") }
    -        it "can match" do
    -          data = subject.match("foofoo,barbaz")
    -          data.mapping["foo"].should == "foo"
    -          data.mapping["bar"].should == "bar"
    -        end
    -        it "lists vars" do
    -          subject.variables.should == ["foo", "bar"]
    -        end
    -      end
    -      context "+ operator" do
    -        subject { Addressable::UriTemplate.new("foo{+foo,bar}baz") }
    -        it "can match" do
    -          data = subject.match("foofoo/bar,barbaz")
    -          data.mapping["bar"].should == "foo/bar,bar"
    -          data.mapping["foo"].should == ""
    -        end
    -        it "lists vars" do
    -          subject.variables.should == ["foo", "bar"]
    -        end
    -      end
    -      context ". operator" do
    -        subject { Addressable::UriTemplate.new("foo{.foo,bar}baz") }
    -        it "can match" do
    -          data = subject.match("foo.foo.barbaz")
    -          data.mapping["foo"].should == "foo"
    -          data.mapping["bar"].should == "bar"
    -        end
    -        it "lists vars" do
    -          subject.variables.should == ["foo", "bar"]
    -        end
    -      end
    -      context "/ operator" do
    -        subject { Addressable::UriTemplate.new("foo{/foo,bar}baz") }
    -        it "can match" do
    -          data = subject.match("foo/foo/barbaz")
    -          data.mapping["foo"].should == "foo"
    -          data.mapping["bar"].should == "bar"
    -        end
    -        it "lists vars" do
    -          subject.variables.should == ["foo", "bar"]
    -        end
    -      end
    -      context "; operator" do
    -        subject { Addressable::UriTemplate.new("foo{;foo,bar,baz}baz") }
    -        it "can match" do
    -          data = subject.match("foo;foo=bar%20baz;bar=foo;bazbaz")
    -          data.mapping["foo"].should == "bar baz"
    -          data.mapping["bar"].should == "foo"
    -          data.mapping["baz"].should == ""
    -        end
    -        it "lists vars" do
    -          subject.variables.should == %w(foo bar baz)
    -        end
    -      end
    -      context "? operator" do
    -        subject { Addressable::UriTemplate.new("foo{?foo,bar}baz") }
    -        it "can match" do
    -          data = subject.match("foo?foo=bar%20baz&bar=foobaz")
    -          data.mapping["foo"].should == "bar baz"
    -          data.mapping["bar"].should == "foo"
    -        end
    -        it "lists vars" do
    -          subject.variables.should == %w(foo bar)
    -        end
    -      end
    -      context "& operator" do
    -        subject { Addressable::UriTemplate.new("foo{&foo,bar}baz") }
    -        it "can match" do
    -          data = subject.match("foo&foo=bar%20baz&bar=foobaz")
    -          data.mapping["foo"].should == "bar baz"
    -          data.mapping["bar"].should == "foo"
    -        end
    -        it "lists vars" do
    -          subject.variables.should == %w(foo bar)
    -        end
    -      end
    -    end
    -  end
    -
    -  context "support regexes:" do
    -    context "EXPRESSION" do
    -      subject { Addressable::UriTemplate::EXPRESSION }
    -      it "should be able to match an expression" do
    -        subject.should match("{foo}")
    -        subject.should match("{foo,9}")
    -        subject.should match("{foo.bar,baz}")
    -        subject.should match("{+foo.bar,baz}")
    -        subject.should match("{foo,foo%20bar}")
    -        subject.should match("{#foo:20,baz*}")
    -        subject.should match("stuff{#foo:20,baz*}things")
    -      end
    -      it "should fail on non vars" do
    -        subject.should_not match("!{foo")
    -        subject.should_not match("{foo.bar.}")
    -        subject.should_not match("!{}")
    -      end
    -    end
    -    context "VARNAME" do
    -      subject { Addressable::UriTemplate::VARNAME }
    -      it "should be able to match a variable" do
    -        subject.should match("foo")
    -        subject.should match("9")
    -        subject.should match("foo.bar")
    -        subject.should match("foo_bar")
    -        subject.should match("foo_bar.baz")
    -        subject.should match("foo%20bar")
    -        subject.should match("foo%20bar.baz")
    -      end
    -      it "should fail on non vars" do
    -        subject.should_not match("!foo")
    -        subject.should_not match("foo.bar.")
    -        subject.should_not match("foo%2%00bar")
    -        subject.should_not match("foo_ba%r")
    -        subject.should_not match("foo_bar*")
    -        subject.should_not match("foo_bar:20")
    -      end
    -    end
    -    context "VARIABLE_LIST" do
    -      subject { Addressable::UriTemplate::VARIABLE_LIST }
    -      it "should be able to match a variable list" do
    -        subject.should match("foo,bar")
    -        subject.should match("foo")
    -        subject.should match("foo,bar*,baz")
    -        subject.should match("foo.bar,bar_baz*,baz:12")
    -      end
    -      it "should fail on non vars" do
    -        subject.should_not match(",foo,bar*,baz")
    -        subject.should_not match("foo,*bar,baz")
    -        subject.should_not match("foo,,bar*,baz")
    -      end
    -    end
    -    context "VARSPEC" do
    -      subject { Addressable::UriTemplate::VARSPEC }
    -      it "should be able to match a variable with modifier" do
    -        subject.should match("9:8")
    -        subject.should match("foo.bar*")
    -        subject.should match("foo_bar:12")
    -        subject.should match("foo_bar.baz*")
    -        subject.should match("foo%20bar:12")
    -        subject.should match("foo%20bar.baz*")
    -      end
    -      it "should fail on non vars" do
    -        subject.should_not match("!foo")
    -        subject.should_not match("*foo")
    -        subject.should_not match("fo*o")
    -        subject.should_not match("fo:o")
    -        subject.should_not match("foo:")
    -      end
    -    end
    -  end
    -end
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

11

News mentions

0

No linked articles in our index yet.