class Addressable::Template
This is an implementation of a URI
template based on RFC 6570 (tools.ietf.org/html/rfc6570).
Constants
- EXPRESSION
- JOINERS
- LEADERS
- RESERVED
- UNRESERVED
- VARIABLE_LIST
- VARNAME
- VARSPEC
Attributes
@return [String] The Template
object’s pattern.
Public Class Methods
Creates a new Addressable::Template
object.
@param [#to_str] pattern The URI
Template
pattern.
@return [Addressable::Template] The initialized Template
object.
# File lib/addressable/template.rb, line 234 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
Public Instance Methods
Returns true
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.
# File lib/addressable/template.rb, line 274 def ==(template) return false unless template.kind_of?(Template) return self.pattern == template.pattern end
Addressable::Template
makes no distinction between ‘==` and `eql?`.
@see ==
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 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::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
# File lib/addressable/template.rb, line 591 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
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"]}
# File lib/addressable/template.rb, line 342 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"]]
# File lib/addressable/template.rb, line 413 def match(uri, processor=nil) uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::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
Returns the named captures of the coerced ‘Regexp`.
@return [Hash] The named captures of the ‘Regexp` given by {#to_regexp}.
@api private
# File lib/addressable/template.rb, line 651 def named_captures self.to_regexp.named_captures 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 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::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"
# File lib/addressable/template.rb, line 524 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
Returns the source of the coerced ‘Regexp`.
@return [String] The source of the ‘Regexp` given by {#to_regexp}.
@api private
# File lib/addressable/template.rb, line 641 def source self.to_regexp.source 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.
# File lib/addressable/template.rb, line 630 def to_regexp _, source = parse_template_pattern(pattern) Regexp.new(source) end
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
# File lib/addressable/template.rb, line 618 def variable_defaults @variable_defaults ||= Hash[*ordered_variable_defaults.reject { |k, v| v.nil? }.flatten] 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.
# File lib/addressable/template.rb, line 607 def variables @variables ||= ordered_variable_defaults.map { |var, val| var }.uniq end
Private Instance Methods
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
# File lib/addressable/template.rb, line 861 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
Generates a hash with string keys
@param [Hash] mapping A mapping hash to normalize
@return [Hash]
A hash with stringified keys
# File lib/addressable/template.rb, line 925 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
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
# File lib/addressable/template.rb, line 898 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
# File lib/addressable/template.rb, line 656 def ordered_variable_defaults @ordered_variable_defaults ||= begin expansions, _ = parse_template_pattern(pattern) expansions.flat_map do |capture| _, _, varlist = *capture.match(EXPRESSION) varlist.split(',').map do |varspec| varspec[VARSPEC, 1] end end 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 [Array, Regexp]
An array of expansion variables nad a regular expression which may be used to parse a template pattern
# File lib/addressable/template.rb, line 969 def parse_new_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 = "\\A#{regexp_string}\\z" return expansions, Regexp.new(regexp_string) end
Generates the Regexp
that parses a template pattern. Memoizes the value if template processor not set (processors may not be deterministic)
@param [String] pattern The URI
template pattern. @param [#match] processor The template processor to use.
@return [Array, Regexp]
An array of expansion variables nad a regular expression which may be used to parse a template pattern
# File lib/addressable/template.rb, line 951 def parse_template_pattern(pattern, processor = nil) if processor.nil? && pattern == @pattern @cached_template_parse ||= parse_new_template_pattern(pattern, processor) else parse_new_template_pattern(pattern, processor) 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 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
# File lib/addressable/template.rb, line 753 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
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 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
# File lib/addressable/template.rb, line 694 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) && !mapping[name].nil? } vars = [first_to_expand] + vars.reject {|varspec| varspec == first_to_expand} if first_to_expand end vars. inject("".dup) do |acc, varspec| _, name, _ = *varspec.match(VARSPEC) next_val = if mapping.key? name transform_capture(mapping, "{#{operator}#{varspec}}", processor, normalize_values) else "{#{operator}#{varspec}}" end # If we've already expanded at least one '?' operator with non-empty # value, change to '&' operator = "&" if (operator == "?") && (next_val != "") acc << next_val end end