diff --git a/lib/addressable/template.rb b/lib/addressable/template.rb index a6488fd6..7901e43d 100644 --- a/lib/addressable/template.rb +++ b/lib/addressable/template.rb @@ -22,15 +22,44 @@ module Addressable ## # This is an implementation of a URI template based on - # URI Template draft 03. + # 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 restore or # match messages or both. The restore 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 restore or # match messages or both. The restore 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 validate or # transform messages or both. Both the validate 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 validate or + # transform messages or both. Both the validate and + # transform methods should take two parameters: name and + # value. The validate method should return true + # or false; true if the value of the variable is valid, + # false otherwise. An InvalidTemplateValueError exception + # will be raised if the value is invalid. The transform method + # should return the transformed variable value as a String. If a + # transform 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 String. + # Generates a hash with string keys # - # @param [String] expansion The operator String. - # @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 diff --git a/lib/addressable/uri_template.rb b/lib/addressable/uri_template.rb deleted file mode 100644 index 69f8bef8..00000000 --- a/lib/addressable/uri_template.rb +++ /dev/null @@ -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 restore or - # match messages or both. The restore method should - # take two parameters: `[String] name` and `[String] value`. - # The restore method should reverse any transformations that - # have been performed on the value to ensure a valid URI. - # The match method should take a single - # parameter: `[String] name`. The match method should return - # a String containing a regular expression capture group for - # matching on that particular variable. The default value is `".*?"`. - # The match method has no effect on multivariate operator - # expansions. - # - # @return [Hash, NilClass] - # The Hash mapping that was extracted from the URI, or - # nil 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 validate or - # transform messages or both. Both the validate and - # transform methods should take two parameters: name and - # value. The validate method should return true - # or false; true if the value of the variable is valid, - # false otherwise. An InvalidTemplateValueError - # exception will be raised if the value is invalid. The transform - # method should return the transformed variable value as a String. - # If a transform 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 validate or - # transform messages or both. Both the validate and - # transform methods should take two parameters: name and - # value. The validate method should return true - # or false; true if the value of the variable is valid, - # false otherwise. An InvalidTemplateValueError - # exception will be raised if the value is invalid. The transform - # method should return the transformed variable value as a String. - # If a transform 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 validate or - # transform messages or both. Both the validate and - # transform methods should take two parameters: name and - # value. The validate method should return true - # or false; true if the value of the variable is valid, - # false otherwise. An InvalidTemplateValueError exception - # will be raised if the value is invalid. The transform method - # should return the transformed variable value as a String. If a - # transform 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 validate or - # transform messages or both. Both the validate and - # transform methods should take two parameters: name and - # value. The validate method should return true - # or false; true if the value of the variable is valid, - # false otherwise. An InvalidTemplateValueError exception - # will be raised if the value is invalid. The transform method - # should return the transformed variable value as a String. If a - # transform 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 Regexp 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 diff --git a/spec/addressable/template_spec.rb b/spec/addressable/template_spec.rb index 1fb41b58..2a5195fb 100644 --- a/spec/addressable/template_spec.rb +++ b/spec/addressable/template_spec.rb @@ -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/{-join|/|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/{-list|/|a,b,c}/" - ).expand({ - "a" => ["a"], "b" => ["b"], "c" => "c" - }) - end).should raise_error( - Addressable::Template::InvalidTemplateOperatorError - ) - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{one}/{two}/" - ) - @partial_template = @initial_template.partial_expand({"one" => "1"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1", "two" => "2"}).should == - @partial_template.expand({"two" => "2"}) - end - - it "should raise an error if the template is expanded with bogus values" do - (lambda do - @initial_template.expand({"one" => Object.new, "two" => Object.new}) - end).should raise_error(TypeError) - (lambda do - @partial_template.expand({"two" => Object.new}) - end).should raise_error(TypeError) - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{one}/{two}/" - ) - @partial_template = @initial_template.partial_expand({"two" => "2"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1", "two" => "2"}).should == - @partial_template.expand({"one" => "1"}) - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{one}/{two}/" - ) - @partial_template = @initial_template.partial_expand({"two" => "2"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1", "two" => "2"}).should == - @partial_template.expand({"one" => "1"}) - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{one=1}/{two=2}/" - ) - @partial_template = @initial_template.partial_expand({"one" => "3"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "3", "two" => "4"}).should == - @partial_template.expand({"two" => "4"}) - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).should === "http://example.com/3/2/" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{one=1}/{two=2}/" - ) - @partial_template = @initial_template.partial_expand({"two" => "4"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "3", "two" => "4"}).should == - @partial_template.expand({"one" => "3"}) - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).should === "http://example.com/1/4/" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{-opt|found|one,two,three}" - ) - @partial_template = @initial_template.partial_expand({}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1"}).to_str.should == - @partial_template.expand({"two" => "2"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"one" => "1"}).to_str.should == - "http://example.com/found" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"two" => "2"}).to_str.should == - "http://example.com/found" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"three" => "3"}).to_str.should == - "http://example.com/found" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"four" => "4"}).to_str.should == - "http://example.com/" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"one" => "1", "two" => "2"}).to_str.should == - "http://example.com/found" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{-opt|found|one,two,three}" - ) - @partial_template = @initial_template.partial_expand({"one" => "1"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1"}).to_str.should == - @partial_template.expand({"two" => "2"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/found" - end - - it "should produce the correct pattern" do - @partial_template.pattern.should == "http://example.com/found" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{-neg|notfound|one,two,three}" - ) - @partial_template = @initial_template.partial_expand({}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1"}).to_str.should == - @partial_template.expand({"two" => "2"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/notfound" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"one" => "1"}).to_str.should == - "http://example.com/" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"two" => "2"}).to_str.should == - "http://example.com/" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"three" => "3"}).to_str.should == - "http://example.com/" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"four" => "4"}).to_str.should == - "http://example.com/notfound" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"one" => "1", "two" => "2"}).to_str.should == - "http://example.com/" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{-neg|notfound|one,two,three}" - ) - @partial_template = @initial_template.partial_expand({"one" => "1"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1"}).to_str.should == - @partial_template.expand({"two" => "2"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/" - end - - it "should produce the correct pattern" do - @partial_template.pattern.should == "http://example.com/" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-prefix|x=|one}" - ) - @partial_template = @initial_template.partial_expand({}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1"}).to_str.should == - @partial_template.expand({"one" => "1"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/?" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"one" => "1"}).to_str.should == - "http://example.com/?x=1" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"two" => "2"}).to_str.should == - "http://example.com/?" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"one" => "1", "two" => "2"}).to_str.should == - "http://example.com/?x=1" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-prefix|x=|one}" - ) - @partial_template = @initial_template.partial_expand({"one" => "1"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1"}).to_str.should == - @partial_template.expand({}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/?x=1" - end - - it "should produce the correct pattern" do - @partial_template.pattern.should == "http://example.com/?x=1" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-suffix|=x|one}" - ) - @partial_template = @initial_template.partial_expand({}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1"}).to_str.should == - @partial_template.expand({"one" => "1"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/?" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"one" => "1"}).to_str.should == - "http://example.com/?1=x" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"two" => "2"}).to_str.should == - "http://example.com/?" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"one" => "1", "two" => "2"}).to_str.should == - "http://example.com/?1=x" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-suffix|=x|one}" - ) - @partial_template = @initial_template.partial_expand({"one" => "1"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1"}).to_str.should == - @partial_template.expand({}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/?1=x" - end - - it "should produce the correct pattern" do - @partial_template.pattern.should == "http://example.com/?1=x" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-join|&|one}" - ) - @partial_template = @initial_template.partial_expand({}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1"}).to_str.should == - @partial_template.expand({"one" => "1"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/?" - end - - it "should produce the correct result when fully expanded" do - @partial_template.pattern.should == @initial_template.pattern - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-join|&|one,two}" - ) - @partial_template = @initial_template.partial_expand({"one" => "1"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1", "two" => "2"}).to_str.should == - @partial_template.expand({"two" => "2"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/?one=1" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-join|&|one,two}" - ) - @partial_template = @initial_template.partial_expand({"two" => "2"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({"one" => "1", "two" => "2"}).to_str.should == - @partial_template.expand({"one" => "1"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/?two=2" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-join|&|one,two,three}" - ) - @partial_template = @initial_template.partial_expand({"one" => "1"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({ - "one" => "1", "two" => "2", "three" => "3" - }).to_str.should == - @partial_template.expand({"two" => "2", "three" => "3"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/?one=1" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"two" => "2"}).to_str.should == - "http://example.com/?one=1&two=2" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"three" => "3"}).to_str.should == - "http://example.com/?one=1&three=3" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-join|&|one,two,three}" - ) - @partial_template = @initial_template.partial_expand({"two" => "2"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({ - "one" => "1", "two" => "2", "three" => "3" - }).to_str.should == - @partial_template.expand({"one" => "1", "three" => "3"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/?two=2" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"one" => "1"}).to_str.should == - "http://example.com/?one=1&two=2" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"three" => "3"}).to_str.should == - "http://example.com/?two=2&three=3" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-join|&|one,two,three}" - ) - @partial_template = @initial_template.partial_expand({"three" => "3"}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({ - "one" => "1", "two" => "2", "three" => "3" - }).to_str.should == - @partial_template.expand({"one" => "1", "two" => "2"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/?three=3" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"one" => "1"}).to_str.should == - "http://example.com/?one=1&three=3" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"two" => "2"}).to_str.should == - "http://example.com/?two=2&three=3" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-join|&|one,two,three}" - ) - @partial_template = @initial_template.partial_expand({ - "one" => "1", "two" => "2" - }) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({ - "one" => "1", "two" => "2", "three" => "3" - }).to_str.should == - @partial_template.expand({"three" => "3"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == - "http://example.com/?one=1&two=2" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"three" => "3"}).to_str.should == - "http://example.com/?one=1&two=2&three=3" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-join|&|one,two,three}" - ) - @partial_template = @initial_template.partial_expand({ - "one" => "1", "three" => "3" - }) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({ - "one" => "1", "two" => "2", "three" => "3" - }).to_str.should == - @partial_template.expand({"two" => "2"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == - "http://example.com/?one=1&three=3" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"two" => "2"}).to_str.should == - "http://example.com/?one=1&two=2&three=3" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-join|&|one,two,three}" - ) - @partial_template = @initial_template.partial_expand({ - "two" => "2", "three" => "3" - }) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({ - "one" => "1", "two" => "2", "three" => "3" - }).to_str.should == - @partial_template.expand({"one" => "1"}).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == - "http://example.com/?two=2&three=3" - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({"one" => "1"}).to_str.should == - "http://example.com/?one=1&two=2&three=3" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/?{-join|&|one,two,three}" - ) - end - - it "should raise an error when partially expanding a bogus operator" do - (lambda do - @initial_template.partial_expand({"one" => ["1"]}) - end).should raise_error( - Addressable::Template::InvalidTemplateOperatorError - ) - (lambda do - @initial_template.partial_expand({"two" => "2", "three" => ["3"]}) - end).should raise_error( - Addressable::Template::InvalidTemplateOperatorError - ) - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{-list|/|numbers}/{-list|/|letters}/" - ) - @partial_template = @initial_template.partial_expand({}) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({ - "numbers" => ["1", "2", "3"], "letters" => ["a", "b", "c"] - }).to_str.should == @partial_template.expand({ - "numbers" => ["1", "2", "3"], "letters" => ["a", "b", "c"] - }).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com///" - end - - it "should produce the correct result when fully expanded" do - @partial_template.pattern.should == @initial_template.pattern - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{-list|/|numbers}/{-list|/|letters}/" - ) - @partial_template = @initial_template.partial_expand({ - "numbers" => ["1", "2", "3"] - }) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({ - "numbers" => ["1", "2", "3"], "letters" => ["a", "b", "c"] - }).to_str.should == @partial_template.expand({ - "letters" => ["a", "b", "c"] - }).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com/1/2/3//" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{-list|/|numbers}/{-list|/|letters}/" - ) - @partial_template = @initial_template.partial_expand({ - "letters" => ["a", "b", "c"] - }) - end - - it "should produce the same result when fully expanded" do - @initial_template.expand({ - "numbers" => ["1", "2", "3"], "letters" => ["a", "b", "c"] - }).to_str.should == @partial_template.expand({ - "numbers" => ["1", "2", "3"] - }).to_str - end - - it "should produce the correct result when fully expanded" do - @partial_template.expand({}).to_str.should == "http://example.com//a/b/c/" - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{-list|/|numbers}/{-list|/|letters}/" - ) - end - - it "should raise an error when partially expanding a bogus operator" do - (lambda do - @initial_template.partial_expand({"numbers" => "1"}) - end).should raise_error( - Addressable::Template::InvalidTemplateOperatorError - ) - (lambda do - @initial_template.partial_expand({"letters" => "a"}) - end).should raise_error( - Addressable::Template::InvalidTemplateOperatorError - ) - end -end - -describe Addressable::Template, "with a partially expanded template" do - before do - @initial_template = Addressable::Template.new( - "http://example.com/{-bogus|/|one,two}/" - ) - end - - it "should raise an error when partially expanding a bogus operator" do - (lambda do - @initial_template.partial_expand({"one" => "1"}) - end).should raise_error( - Addressable::Template::InvalidTemplateOperatorError - ) + context "support regexes:" do + context "EXPRESSION" do + subject { Addressable::Template::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::Template::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::Template::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::Template::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 diff --git a/spec/addressable/uri_template_spec.rb b/spec/addressable/uri_template_spec.rb deleted file mode 100644 index b72ee622..00000000 --- a/spec/addressable/uri_template_spec.rb +++ /dev/null @@ -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