blob: feee3f17d45aeaef61652a775133d181ac9d2962 [file] [log] [blame]
# encoding:utf-8
#--
# Copyright (C) Bob Aman
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#++
require "addressable/version"
require "addressable/uri"
module Addressable
##
# This is an implementation of a URI template based on
# 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
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.
class InvalidTemplateValueError < StandardError
end
##
# Raised if an invalid template operator is used in a pattern.
class InvalidTemplateOperatorError < StandardError
end
##
# Raised if an invalid template operator is used in a pattern.
class TemplateOperatorAbortedError < StandardError
end
##
# This class represents the data that is extracted when a Template
# is matched against a URI.
class MatchData
##
# Creates a new MatchData object.
# MatchData objects should never be instantiated directly.
#
# @param [Addressable::URI] uri
# The URI that the template was matched against.
def initialize(uri, template, mapping)
@uri = uri.dup.freeze
@template = template
@mapping = mapping.dup.freeze
end
##
# @return [Addressable::URI]
# The URI that the Template was matched against.
attr_reader :uri
##
# @return [Addressable::Template]
# The Template used for the match.
attr_reader :template
##
# @return [Hash]
# The mapping that resulted from the match.
# Note that this mapping does not include keys or values for
# variables that appear in the Template, but are not present
# in the URI.
attr_reader :mapping
##
# @return [Array]
# The list of variables that were present in the Template.
# Note that this list will include variables which do not appear
# in the mapping because they were not present in URI.
def variables
self.template.variables
end
alias_method :keys, :variables
alias_method :names, :variables
##
# @return [Array]
# The list of values that were captured by the Template.
# Note that this list will include nils for any variables which
# were in the Template, but did not appear in the URI.
def values
@values ||= self.variables.inject([]) do |accu, key|
accu << self.mapping[key]
accu
end
end
alias_method :captures, :values
##
# Accesses captured values by name or by index.
#
# @param [String, Symbol, Fixnum] key
# Capture index or name. Note that when accessing by with index
# of 0, the full URI will be returned. The intention is to mimic
# the ::MatchData#[] behavior.
#
# @param [#to_int, nil] len
# If provided, an array of values will be returend with the given
# parameter used as length.
#
# @return [Array, String, nil]
# The captured value corresponding to the index or name. If the
# value was not provided or the key is unknown, nil will be
# returned.
#
# If the second parameter is provided, an array of that length will
# be returned instead.
def [](key, len = nil)
if len
to_a[key, len]
elsif String === key or Symbol === key
mapping[key.to_s]
else
to_a[key]
end
end
##
# @return [Array]
# Array with the matched URI as first element followed by the captured
# values.
def to_a
[to_s, *values]
end
##
# @return [String]
# The matched URI as String.
def to_s
uri.to_s
end
alias_method :string, :to_s
# Returns multiple captured values at once.
#
# @param [String, Symbol, Fixnum] *indexes
# Indices of the captures to be returned
#
# @return [Array]
# Values corresponding to given indices.
#
# @see Addressable::Template::MatchData#[]
def values_at(*indexes)
indexes.map { |i| self[i] }
end
##
# Returns a <tt>String</tt> representation of the MatchData's state.
#
# @return [String] The MatchData's state, as a <tt>String</tt>.
def inspect
sprintf("#<%s:%#0x RESULT:%s>",
self.class.to_s, self.object_id, self.mapping.inspect)
end
##
# Dummy method for code expecting a ::MatchData instance
#
# @return [String] An empty string.
def pre_match
""
end
alias_method :post_match, :pre_match
end
##
# Creates a new <tt>Addressable::Template</tt> object.
#
# @param [#to_str] pattern The URI Template pattern.
#
# @return [Addressable::Template] The initialized Template object.
def initialize(pattern)
if !pattern.respond_to?(:to_str)
raise TypeError, "Can't convert #{pattern.class} into String."
end
@pattern = pattern.to_str.dup.freeze
end
##
# Freeze URI, initializing instance variables.
#
# @return [Addressable::URI] The frozen URI object.
def freeze
self.variables
self.variable_defaults
self.named_captures
super
end
##
# @return [String] The Template object's pattern.
attr_reader :pattern
##
# Returns a <tt>String</tt> representation of the Template object's state.
#
# @return [String] The Template object's state, as a <tt>String</tt>.
def inspect
sprintf("#<%s:%#0x PATTERN:%s>",
self.class.to_s, self.object_id, self.pattern)
end
##
# Returns <code>true</code> if the Template objects are equal. This method
# does NOT normalize either Template before doing the comparison.
#
# @param [Object] template The Template to compare.
#
# @return [TrueClass, FalseClass]
# <code>true</code> if the Templates are equivalent, <code>false</code>
# otherwise.
def ==(template)
return false unless template.kind_of?(Template)
return self.pattern == template.pattern
end
##
# Addressable::Template makes no distinction between `==` and `eql?`.
#
# @see #==
alias_method :eql?, :==
##
# Extracts a mapping from the URI using a URI Template pattern.
#
# @param [Addressable::URI, #to_str] uri
# The URI to extract from.
#
# @param [#restore, #match] processor
# A template processor object may optionally be supplied.
#
# The object should respond to either the <tt>restore</tt> or
# <tt>match</tt> messages or both. The <tt>restore</tt> method should
# take two parameters: `[String] name` and `[String] value`.
# The <tt>restore</tt> method should reverse any transformations that
# have been performed on the value to ensure a valid URI.
# The <tt>match</tt> method should take a single
# parameter: `[String] name`. The <tt>match</tt> method should return
# a <tt>String</tt> containing a regular expression capture group for
# matching on that particular variable. The default value is `".*?"`.
# The <tt>match</tt> method has no effect on multivariate operator
# expansions.
#
# @return [Hash, NilClass]
# The <tt>Hash</tt> mapping that was extracted from the URI, or
# <tt>nil</tt> if the URI didn't match the template.
#
# @example
# class ExampleProcessor
# def self.restore(name, value)
# return value.gsub(/\+/, " ") if name == "query"
# return value
# end
#
# def self.match(name)
# return ".*?" if name == "first"
# return ".*"
# end
# end
#
# uri = Addressable::URI.parse(
# "http://example.com/search/an+example+search+query/"
# )
# Addressable::Template.new(
# "http://example.com/search/{query}/"
# ).extract(uri, ExampleProcessor)
# #=> {"query" => "an example search query"}
#
# uri = Addressable::URI.parse("http://example.com/a/b/c/")
# Addressable::Template.new(
# "http://example.com/{first}/{second}/"
# ).extract(uri, ExampleProcessor)
# #=> {"first" => "a", "second" => "b/c"}
#
# uri = Addressable::URI.parse("http://example.com/a/b/c/")
# Addressable::Template.new(
# "http://example.com/{first}/{-list|/|second}/"
# ).extract(uri)
# #=> {"first" => "a", "second" => ["b", "c"]}
def extract(uri, processor=nil)
match_data = self.match(uri, processor)
return (match_data ? match_data.mapping : nil)
end
##
# Extracts match data from the URI using a URI Template pattern.
#
# @param [Addressable::URI, #to_str] uri
# The URI to extract from.
#
# @param [#restore, #match] processor
# A template processor object may optionally be supplied.
#
# The object should respond to either the <tt>restore</tt> or
# <tt>match</tt> messages or both. The <tt>restore</tt> method should
# take two parameters: `[String] name` and `[String] value`.
# The <tt>restore</tt> method should reverse any transformations that
# have been performed on the value to ensure a valid URI.
# The <tt>match</tt> method should take a single
# parameter: `[String] name`. The <tt>match</tt> method should return
# a <tt>String</tt> containing a regular expression capture group for
# matching on that particular variable. The default value is `".*?"`.
# The <tt>match</tt> method has no effect on multivariate operator
# expansions.
#
# @return [Hash, NilClass]
# The <tt>Hash</tt> mapping that was extracted from the URI, or
# <tt>nil</tt> if the URI didn't match the template.
#
# @example
# class ExampleProcessor
# def self.restore(name, value)
# return value.gsub(/\+/, " ") if name == "query"
# return value
# end
#
# def self.match(name)
# return ".*?" if name == "first"
# return ".*"
# end
# end
#
# uri = Addressable::URI.parse(
# "http://example.com/search/an+example+search+query/"
# )
# match = Addressable::Template.new(
# "http://example.com/search/{query}/"
# ).match(uri, ExampleProcessor)
# match.variables
# #=> ["query"]
# match.captures
# #=> ["an example search query"]
#
# uri = Addressable::URI.parse("http://example.com/a/b/c/")
# match = Addressable::Template.new(
# "http://example.com/{first}/{+second}/"
# ).match(uri, ExampleProcessor)
# match.variables
# #=> ["first", "second"]
# match.captures
# #=> ["a", "b/c"]
#
# uri = Addressable::URI.parse("http://example.com/a/b/c/")
# match = Addressable::Template.new(
# "http://example.com/{first}{/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)
return nil unless uri.to_str.match(expansion_regexp)
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)
mapping[name] ||= nil
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 == '*'
if unparsed_values[index]
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
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.has_key?(name) || mapping[name].nil?
# Doesn't exist, set to value (even if value is nil)
mapping[name] = value
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.
# @param [Boolean] normalize_values
# Optional flag to enable/disable unicode normalization. Default: true
#
# The object should respond to either the <tt>validate</tt> or
# <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
# <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
# <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
# or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
# <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
# exception will be raised if the value is invalid. The <tt>transform</tt>
# method should return the transformed variable value as a <tt>String</tt>.
# If a <tt>transform</tt> method is used, the value will not be percent
# encoded automatically. Unicode normalization will be performed both
# before and after sending the value to the transform method.
#
# @return [Addressable::Template] The partially expanded URI template.
#
# @example
# Addressable::Template.new(
# "http://example.com/{one}/{two}/"
# ).partial_expand({"one" => "1"}).pattern
# #=> "http://example.com/1/{two}/"
#
# Addressable::Template.new(
# "http://example.com/{?one,two}/"
# ).partial_expand({"one" => "1"}).pattern
# #=> "http://example.com/?one=1{&two}/"
#
# Addressable::Template.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, normalize_values=true)
result = self.pattern.dup
mapping = normalize_keys(mapping)
result.gsub!( EXPRESSION ) do |capture|
transform_partial_capture(mapping, capture, processor, normalize_values)
end
return Addressable::Template.new(result)
end
##
# Expands a URI template into a full URI.
#
# @param [Hash] mapping The mapping that corresponds to the pattern.
# @param [#validate, #transform] processor
# An optional processor object may be supplied.
# @param [Boolean] normalize_values
# Optional flag to enable/disable unicode normalization. Default: true
#
# The object should respond to either the <tt>validate</tt> or
# <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
# <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
# <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
# or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
# <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
# exception will be raised if the value is invalid. The <tt>transform</tt>
# method should return the transformed variable value as a <tt>String</tt>.
# If a <tt>transform</tt> method is used, the value will not be percent
# encoded automatically. Unicode normalization will be performed both
# before and after sending the value to the transform method.
#
# @return [Addressable::URI] The expanded URI template.
#
# @example
# class ExampleProcessor
# def self.validate(name, value)
# return !!(value =~ /^[\w ]+$/) if name == "query"
# return true
# end
#
# def self.transform(name, value)
# return value.gsub(/ /, "+") if name == "query"
# return value
# end
# end
#
# Addressable::Template.new(
# "http://example.com/search/{query}/"
# ).expand(
# {"query" => "an example search query"},
# ExampleProcessor
# ).to_str
# #=> "http://example.com/search/an+example+search+query/"
#
# Addressable::Template.new(
# "http://example.com/search/{query}/"
# ).expand(
# {"query" => "an example search query"}
# ).to_str
# #=> "http://example.com/search/an%20example%20search%20query/"
#
# Addressable::Template.new(
# "http://example.com/search/{query}/"
# ).expand(
# {"query" => "bogus!"},
# ExampleProcessor
# ).to_str
# #=> Addressable::Template::InvalidTemplateValueError
def expand(mapping, processor=nil, normalize_values=true)
result = self.pattern.dup
mapping = normalize_keys(mapping)
result.gsub!( EXPRESSION ) do |capture|
transform_capture(mapping, capture, processor, normalize_values)
end
return Addressable::URI.parse(result)
end
##
# Returns an Array of variables used within the template pattern.
# The variables are listed in the Array in the order they appear within
# the pattern. Multiple occurrences of a variable within a pattern are
# not represented in this Array.
#
# @return [Array] The variables present in the template's pattern.
def variables
@variables ||= ordered_variable_defaults.map { |var, val| var }.uniq
end
alias_method :keys, :variables
alias_method :names, :variables
##
# Returns a mapping of variables to their default values specified
# in the template. Variables without defaults are not returned.
#
# @return [Hash] Mapping of template variables to their defaults
def variable_defaults
@variable_defaults ||=
Hash[*ordered_variable_defaults.reject { |k, v| v.nil? }.flatten]
end
##
# Coerces a template into a `Regexp` object. This regular expression will
# behave very similarly to the actual template, and should match the same
# URI values, but it cannot fully handle, for example, values that would
# extract to an `Array`.
#
# @return [Regexp] A regular expression which should match the template.
def to_regexp
_, source = parse_template_pattern(pattern)
Regexp.new(source)
end
##
# Returns the source of the coerced `Regexp`.
#
# @return [String] The source of the `Regexp` given by {#to_regexp}.
#
# @api private
def source
self.to_regexp.source
end
##
# Returns the named captures of the coerced `Regexp`.
#
# @return [Hash] The named captures of the `Regexp` given by {#to_regexp}.
#
# @api private
def named_captures
self.to_regexp.named_captures
end
##
# Generates a route result for a given set of parameters.
# Should only be used by rack-mount.
#
# @param params [Hash] The set of parameters used to expand the template.
# @param recall [Hash] Default parameters used to expand the template.
# @param options [Hash] Either a `:processor` or a `:parameterize` block.
#
# @api private
def generate(params={}, recall={}, options={})
merged = recall.merge(params)
if options[:processor]
processor = options[:processor]
elsif options[:parameterize]
# TODO: This is sending me into fits trying to shoe-horn this into
# the existing API. I think I've got this backwards and processors
# should be a set of 4 optional blocks named :validate, :transform,
# :match, and :restore. Having to use a singleton here is a huge
# code smell.
processor = Object.new
class <<processor
attr_accessor :block
def transform(name, value)
block.call(name, value)
end
end
processor.block = options[:parameterize]
else
processor = nil
end
result = self.expand(merged, processor)
result.to_s if result
end
private
def ordered_variable_defaults
@ordered_variable_defaults ||= begin
expansions, _ = parse_template_pattern(pattern)
expansions.map do |capture|
_, _, varlist = *capture.match(EXPRESSION)
varlist.split(',').map do |varspec|
varspec[VARSPEC, 1]
end
end.flatten
end
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.
# @param [Boolean] normalize_values
# Optional flag to enable/disable unicode normalization. Default: true
#
# The object should respond to either the <tt>validate</tt> or
# <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
# <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
# <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
# or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
# <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt> exception
# will be raised if the value is invalid. The <tt>transform</tt> method
# should return the transformed variable value as a <tt>String</tt>. If a
# <tt>transform</tt> method is used, the value will not be percent encoded
# automatically. Unicode normalization will be performed both before and
# after sending the value to the transform method.
#
# @return [String] The expanded expression
def transform_partial_capture(mapping, capture, processor = nil,
normalize_values = true)
_, operator, varlist = *capture.match(EXPRESSION)
vars = varlist.split(',')
if '?' == operator
# partial expansion of form style query variables sometimes requires a
# slight reordering of the variables to produce a valid url.
first_to_expand = vars.find { |varspec|
_, name, _ = *varspec.match(VARSPEC)
mapping.key? name
}
vars = [first_to_expand] + vars.reject {|varspec| varspec == first_to_expand} if first_to_expand
end
vars
.zip(operator_sequence(operator).take(vars.length))
.reduce("".dup) do |acc, (varspec, op)|
_, name, _ = *varspec.match(VARSPEC)
acc << if mapping.key? name
transform_capture(mapping, "{#{op}#{varspec}}",
processor, normalize_values)
else
"{#{op}#{varspec}}"
end
end
end
##
# Creates a lazy Enumerator of the operators that should be used to expand
# variables in a varlist starting with `operator`. For example, an operator
# `"?"` results in the sequence `"?","&","&"...`
#
# @param [String] operator from which to generate a sequence
#
# @return [Enumerator] sequence of operators
def operator_sequence(operator)
rest_operator = if "?" == operator
"&"
else
operator
end
head_operator = operator
Enumerator.new do |y|
y << head_operator.to_s
while true
y << rest_operator.to_s
end
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.
# @param [Boolean] normalize_values
# Optional flag to enable/disable unicode normalization. Default: true
#
#
# The object should respond to either the <tt>validate</tt> or
# <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
# <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
# <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
# or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
# <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt> exception
# will be raised if the value is invalid. The <tt>transform</tt> method
# should return the transformed variable value as a <tt>String</tt>. If a
# <tt>transform</tt> method is used, the value will not be percent encoded
# automatically. Unicode normalization will be performed both before and
# after sending the value to the transform method.
#
# @return [String] The expanded expression
def transform_capture(mapping, capture, processor=nil,
normalize_values=true)
_, 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)
# Common primitives where the .to_s output is well-defined
if Numeric === value || Symbol === value ||
value == true || value == false
value = value.to_s
end
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 normalize_values
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)
if normalize_values
transformed_value = normalize_value(transformed_value)
end
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{|inner_value| "#{k}=#{inner_value}"}.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{|inner_value| "#{k}=#{inner_value}"}.join(";")
else
v && v != '' ? ";#{k}=#{v}" : ";#{k}"
end
}.join
else
leader + return_value.map{|k,v| v}.join(joiner)
end
end
##
# Takes a set of values, and joins them together based on the
# operator.
#
# @param [Hash, Array, String] value
# Normalizes keys and values with IDNA#unicode_normalize_kc
#
# @return [Hash, Array, String] The normalized values
def normalize_value(value)
unless value.is_a?(Hash)
value = value.respond_to?(:to_ary) ? value.to_ary : value.to_str
end
# Handle unicode normalization
if value.kind_of?(Array)
value.map! { |val| Addressable::IDNA.unicode_normalize_kc(val) }
elsif value.kind_of?(Hash)
value = value.inject({}) { |acc, (k, v)|
acc[Addressable::IDNA.unicode_normalize_kc(k)] =
Addressable::IDNA.unicode_normalize_kc(v)
acc
}
else
value = Addressable::IDNA.unicode_normalize_kc(value)
end
value
end
##
# Generates a hash with string keys
#
# @param [Hash] mapping A mapping hash to normalize
#
# @return [Hash]
# A hash with stringified keys
def normalize_keys(mapping)
return mapping.inject({}) do |accu, pair|
name, value = pair
if Symbol === name
name = name.to_s
elsif name.respond_to?(:to_str)
name = name.to_str
else
raise TypeError,
"Can't convert #{name.class} into String."
end
accu[name] = value
accu
end
end
##
# Generates the <tt>Regexp</tt> that parses a template pattern.
#
# @param [String] pattern The URI template pattern.
# @param [#match] processor The template processor to use.
#
# @return [Regexp]
# A regular expression which may be used to parse a template pattern.
def parse_template_pattern(pattern, processor=nil)
# Escape the pattern. The two gsubs restore the escaped curly braces
# back to their original form. Basically, escape everything that isn't
# within an expansion.
escaped_pattern = Regexp.escape(
pattern
).gsub(/\\\{(.*?)\\\}/) do |escaped|
escaped.gsub(/\\(.)/, "\\1")
end
expansions = []
# Create a regular expression that captures the values of the
# variables in the URI.
regexp_string = escaped_pattern.gsub( EXPRESSION ) do |expansion|
expansions << expansion
_, operator, varlist = *expansion.match(EXPRESSION)
leader = Regexp.escape(LEADERS.fetch(operator, ''))
joiner = Regexp.escape(JOINERS.fetch(operator, ','))
combined = varlist.split(',').map do |varspec|
_, name, modifier = *varspec.match(VARSPEC)
result = processor && processor.respond_to?(:match) ? processor.match(name) : nil
if result
"(?<#{name}>#{ result })"
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 == '*'
"(?<#{name}>#{group}(?:#{joiner}?#{group})*)?"
else
"(?<#{name}>#{group})?"
end
end
end.join("#{joiner}?")
"(?:|#{leader}#{combined})"
end
# Ensure that the regular expression matches the whole URI.
regexp_string = "^#{regexp_string}$"
return expansions, Regexp.new(regexp_string)
end
end
end