| # 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 |