Class: Addressable::Template

Inherits:
Object
  • Object
show all
Defined in:
lib/addressable/template.rb

Overview

This is an implementation of a URI template based on RFC 6570 (tools.ietf.org/html/rfc6570).

Defined Under Namespace

Classes: InvalidTemplateOperatorError, InvalidTemplateValueError, MatchData, TemplateOperatorAbortedError

Constant Summary collapse

RESERVED =
"(?:[#{anything}]|%[a-fA-F0-9][a-fA-F0-9])"
UNRESERVED =
"(?:[#{
  Addressable::URI::CharacterClasses::UNRESERVED
}]|%[a-fA-F0-9][a-fA-F0-9])"
VARNAME =
/^#{variable}$/
VARSPEC =
/^#{varspec}$/
VARIABLE_LIST =
/^#{varspec}(?:,#{varspec})*$/
EXPRESSION =
/\{([#{operator}])?(#{varspec}(?:,#{varspec})*)\}/
LEADERS =
{
  '?' => '?',
  '/' => '/',
  '#' => '#',
  '.' => '.',
  ';' => ';',
  '&' => '&'
}
JOINERS =
{
  '?' => '&',
  '.' => '.',
  ';' => ';',
  '&' => '&',
  '/' => '/'
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pattern) ⇒ Addressable::Template

Creates a new Addressable::Template object.

Parameters:

  • pattern (#to_str)

    The URI Template pattern.


235
236
237
238
239
240
# File 'lib/addressable/template.rb', line 235

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

Instance Attribute Details

#patternString (readonly)

Returns The Template object's pattern.

Returns:

  • (String)

    The Template object's pattern.


255
256
257
# File 'lib/addressable/template.rb', line 255

def pattern
  @pattern
end

Instance Method Details

#==(template) ⇒ TrueClass, FalseClass Also known as: eql?

Returns true if the Template objects are equal. This method does NOT normalize either Template before doing the comparison.

Parameters:

  • template (Object)

    The Template to compare.

Returns:

  • (TrueClass, FalseClass)

    true if the Templates are equivalent, false otherwise.


275
276
277
278
# File 'lib/addressable/template.rb', line 275

def ==(template)
  return false unless template.kind_of?(Template)
  return self.pattern == template.pattern
end

#expand(mapping, processor = nil, normalize_values = true) ⇒ Addressable::URI

Expands a URI template into a full URI.

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.

Examples:

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

Parameters:

  • mapping (Hash)

    The mapping that corresponds to the pattern.

  • processor (#validate, #transform) (defaults to: nil)

    An optional processor object may be supplied.

  • normalize_values (Boolean) (defaults to: true)

    Optional flag to enable/disable unicode normalization. Default: true

Returns:


592
593
594
595
596
597
598
599
# File 'lib/addressable/template.rb', line 592

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

#extract(uri, processor = nil) ⇒ Hash, NilClass

Extracts a mapping from the URI using a URI Template pattern.

Examples:

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"]}

Parameters:

  • uri (Addressable::URI, #to_str)

    The URI to extract from.

  • processor (#restore, #match) (defaults to: nil)

    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.

Returns:

  • (Hash, NilClass)

    The Hash mapping that was extracted from the URI, or nil if the URI didn't match the template.


343
344
345
346
# File 'lib/addressable/template.rb', line 343

def extract(uri, processor=nil)
  match_data = self.match(uri, processor)
  return (match_data ? match_data.mapping : nil)
end

#freezeAddressable::URI

Freeze URI, initializing instance variables.

Returns:


246
247
248
249
250
251
# File 'lib/addressable/template.rb', line 246

def freeze
  self.variables
  self.variable_defaults
  self.named_captures
  super
end

#generate(params = {}, recall = {}, options = {}) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Generates a route result for a given set of parameters. Should only be used by rack-mount.

Parameters:

  • params (Hash) (defaults to: {})

    The set of parameters used to expand the template.

  • recall (Hash) (defaults to: {})

    Default parameters used to expand the template.

  • options (Hash) (defaults to: {})

    Either a `:processor` or a `:parameterize` block.


665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
# File 'lib/addressable/template.rb', line 665

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

#inspectString

Returns a String representation of the Template object's state.

Returns:

  • (String)

    The Template object's state, as a String.


261
262
263
264
# File 'lib/addressable/template.rb', line 261

def inspect
  sprintf("#<%s:%#0x PATTERN:%s>",
    self.class.to_s, self.object_id, self.pattern)
end

#match(uri, processor = nil) ⇒ Hash, NilClass

Extracts match data from the URI using a URI Template pattern.

Examples:

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"]]

Parameters:

  • uri (Addressable::URI, #to_str)

    The URI to extract from.

  • processor (#restore, #match) (defaults to: nil)

    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.

Returns:

  • (Hash, NilClass)

    The Hash mapping that was extracted from the URI, or nil if the URI didn't match the template.


414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# File 'lib/addressable/template.rb', line 414

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

#named_capturesHash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the named captures of the coerced `Regexp`.

Returns:

  • (Hash)

    The named captures of the `Regexp` given by #to_regexp.


652
653
654
# File 'lib/addressable/template.rb', line 652

def named_captures
  self.to_regexp.named_captures
end

#partial_expand(mapping, processor = nil, normalize_values = true) ⇒ Addressable::Template

Expands a URI template into another URI template.

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.

Examples:

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"

Parameters:

  • mapping (Hash)

    The mapping that corresponds to the pattern.

  • processor (#validate, #transform) (defaults to: nil)

    An optional processor object may be supplied.

  • normalize_values (Boolean) (defaults to: true)

    Optional flag to enable/disable unicode normalization. Default: true

Returns:


525
526
527
528
529
530
531
532
# File 'lib/addressable/template.rb', line 525

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

#sourceString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the source of the coerced `Regexp`.

Returns:

  • (String)

    The source of the `Regexp` given by #to_regexp.


642
643
644
# File 'lib/addressable/template.rb', line 642

def source
  self.to_regexp.source
end

#to_regexpRegexp

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`.

Returns:

  • (Regexp)

    A regular expression which should match the template.


631
632
633
634
# File 'lib/addressable/template.rb', line 631

def to_regexp
  _, source = parse_template_pattern(pattern)
  Regexp.new(source)
end

#variable_defaultsHash

Returns a mapping of variables to their default values specified in the template. Variables without defaults are not returned.

Returns:

  • (Hash)

    Mapping of template variables to their defaults


619
620
621
622
# File 'lib/addressable/template.rb', line 619

def variable_defaults
  @variable_defaults ||=
    Hash[*ordered_variable_defaults.reject { |k, v| v.nil? }.flatten]
end

#variablesArray Also known as: keys, names

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.

Returns:

  • (Array)

    The variables present in the template's pattern.


608
609
610
# File 'lib/addressable/template.rb', line 608

def variables
  @variables ||= ordered_variable_defaults.map { |var, val| var }.uniq
end