module URITemplate

A base module for all implementations of a uri template.

The MIT License (MIT)

Copyright © 2011-2014 Hannes Georg

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Constants

HOST_REGEX

@api private

SCHEME_REGEX

@api private

URI_SPLIT

@api private

VERSIONS

A hash with all available implementations. @see resolve_class

Public Class Methods

apply(a, method, b, *args) click to toggle source

Applies a method to a URITemplate with another URITemplate as argument. This is a useful shorthand since both URITemplates are automatically coerced.

@example

tpl = URITemplate.new('foo')
URITemplate.apply( tpl, :/, 'bar' ).pattern #=> 'foo/bar'
URITemplate.apply( 'baz', :/, tpl ).pattern #=> 'baz/foo'
URITemplate.apply( 'bla', :/, 'blub' ).pattern #=> 'bla/blub'
# File lib/uri_template.rb, line 166
def self.apply(a, method, b, *args)
  a,b,_,_ = self.coerce(a,b)
  a.send(method,b,*args)
end
coerce(a,b) click to toggle source

Tries to coerce two URITemplates into a common representation. Returns an array with two {URITemplate}s and two booleans indicating which of the two were converted or raises an ArgumentError.

@example

URITemplate.coerce( URITemplate.new(:rfc6570,'{x}'), '{y}' ) #=> [URITemplate.new(:rfc6570,'{x}'), URITemplate.new(:rfc6570,'{y}'), false, true]
URITemplate.coerce( '{y}', URITemplate.new(:rfc6570,'{x}') ) #=> [URITemplate.new(:rfc6570,'{y}'), URITemplate.new(:rfc6570,'{x}'), true, false]

@return [Tuple<URITemplate,URITemplate,Bool,Bool>]

# File lib/uri_template.rb, line 130
def self.coerce(a,b)
  if a.kind_of? URITemplate
    if a.class == b.class
      return [a,b,false,false]
    end
    b_as_a = a.class.try_convert(b)
    if b_as_a
      return [a,b_as_a,false,true]
    end
  end
  if b.kind_of? URITemplate
    a_as_b = b.class.try_convert(a)
    if a_as_b
      return [a_as_b, b, true, false]
    end
  end
  bc = self.try_convert(b)
  if bc.kind_of? URITemplate
    a_as_b = bc.class.try_convert(a)
    if a_as_b
      return [a_as_b, bc, true, true]
    end
  end
  raise ArgumentError, "Expected at least on URITemplate, but got #{a.inspect} and #{b.inspect}" unless a.kind_of? URITemplate or b.kind_of? URITemplate
  raise ArgumentError, "Cannot coerce #{a.inspect} and #{b.inspect} into a common representation."
end
coerce_first_arg(meth) click to toggle source

@api private

# File lib/uri_template.rb, line 172
  def self.coerce_first_arg(meth)
    alias_method( (meth.to_s + '_without_coercion').to_sym , meth )
    class_eval(<<-RUBY)
def #{meth}(other, *args, &block)
  this, other, this_converted, _ = URITemplate.coerce( self, other )
  if this_converted
    return this.#{meth}(other,*args, &block)
  end
  return #{meth}_without_coercion(other,*args, &block)
end
RUBY
  end
new(*args) click to toggle source

Creates an uri template using an implementation. The args should at least contain a pattern string. Symbols in the args are used to determine the actual implementation.

@example

tpl = URITemplate.new('{x}') # a new template using the default implementation
tpl.expand('x'=>'y') #=> 'y'

@example

tpl = URITemplate.new(:colon,'/:x') # a new template using the colon implementation
# File lib/uri_template.rb, line 117
def self.new(*args)
  klass, rest = resolve_class(*args)
  return klass.new(*rest)
end
resolve_class(*args) click to toggle source

Looks up which implementation to use. Extracts all symbols from args and looks up the first in {VERSIONS}.

@return Array an array of the class to use and the unused parameters.

@example

URITemplate.resolve_class() #=> [ URITemplate::RFC6570, [] ]
URITemplate.resolve_class(:colon) #=> [ URITemplate::Colon, [] ]
URITemplate.resolve_class("template",:rfc6570) #=> [ URITemplate::RFC6570, ["template"] ]

@raise ArgumentError when no class was found.

# File lib/uri_template.rb, line 99
def self.resolve_class(*args)
  symbols, rest = args.partition{|x| x.kind_of? Symbol }
  version = symbols.fetch(0, :default)
  raise ArgumentError, "Unknown template version #{version.inspect}, defined versions: #{VERSIONS.keys.inspect}" unless VERSIONS.key?(version)
  return self.const_get(VERSIONS[version]), rest
end

Public Instance Methods

+(other)
Alias for: concat
/(other)
Alias for: path_concat
==(other)
Alias for: eq
>>(other)
Alias for: concat
absolute?()
Alias for: host?
concat(other) click to toggle source

Concatenate two template with conversion.

@example

tpl = URITemplate::RFC6570.new('foo')
(tpl + '{bar}' ).pattern #=> 'foo{bar}'

@param other [URITemplate, String, …] @return URITemplate

# File lib/uri_template.rb, line 383
def concat(other)
  if other.host? or other.scheme?
    raise ArgumentError, "Expected to receive a relative template but got an absoulte one: #{other.inspect}. If you think this is a bug, please report it."
  end

  return self if other.tokens.none?
  return other if self.tokens.none?
  return self.class.new( self.to_s + other.to_s )
end
Also aliased as: +, >>
eq(other) click to toggle source

Compares two template patterns.

# File lib/uri_template.rb, line 330
def eq(other)
  return self.pattern == other.pattern
end
Also aliased as: ==
expand(variables = {}) click to toggle source

Expands this uri template with the given variables. The variables should be converted to strings using {Utils#object_to_param}.

The keys in the variables hash are converted to strings in order to support the Ruby 1.9 hash syntax.

If the variables are given as an array, they will be matched against the variables in the template based on their order.

@raise {Unconvertable} if a variable could not be converted to a string. @raise {InvalidValue} if a value is not suiteable for a certain variable ( e.g. a string when a list is expected ).

@param variables [#map, Array] @return String

# File lib/uri_template.rb, line 207
def expand(variables = {})
  variables = normalize_variables(variables)
  tokens.map{|part|
    part.expand(variables)
  }.join
end
expand_partial(variables = {}) click to toggle source

Works like expand with two differences:

- the result is a uri template instead of string
- undefined variables are left in the template

@see {#expand} @param variables [#map, Array] @return [URITemplate]

# File lib/uri_template.rb, line 222
def expand_partial(variables = {})
  variables = normalize_variables(variables)
  self.class.new(tokens.map{|part|
    part.expand_partial(variables)
  }.flatten(1))
end
host?() click to toggle source

Returns whether this uri-template includes a host name

This method is usefull to check wheter this template will generate or match a uri with a host.

@see scheme?

@example

URITemplate.new('/foo').host? #=> false
URITemplate.new('//example.com/foo').host? #=> true
URITemplate.new('//{host}/foo').host? #=> true
URITemplate.new('http://example.com/foo').host? #=> true
URITemplate.new('{scheme}://example.com/foo').host? #=> true
# File lib/uri_template.rb, line 299
def host?
  return scheme_and_host[1]
end
Also aliased as: absolute?
path_concat(other) click to toggle source

Tries to concatenate two templates, as if they were path segments. Removes double slashes or insert one if they are missing.

@example

tpl = URITemplate::RFC6570.new('/xy/')
(tpl / '/z/' ).pattern #=> '/xy/z/'
(tpl / 'z/' ).pattern #=> '/xy/z/'
(tpl / '{/z}' ).pattern #=> '/xy{/z}'
(tpl / 'a' / 'b' ).pattern #=> '/xy/a/b'

@param other [URITemplate, String, …] @return URITemplate

# File lib/uri_template.rb, line 350
def path_concat(other)
  if other.host? or other.scheme?
    raise ArgumentError, "Expected to receive a relative template but got an absoulte one: #{other.inspect}. If you think this is a bug, please report it."
  end

  return self if other.tokens.none?
  return other if self.tokens.none?

  tail = self.tokens.last
  head = other.tokens.first

  if tail.ends_with_slash?
    if head.starts_with_slash?
      return self.class.new( remove_double_slash(self.tokens, other.tokens).join )
    end
  elsif !head.starts_with_slash?
    return self.class.new( (self.tokens + ['/'] + other.tokens).join)
  end
  return self.class.new( (self.tokens + other.tokens).join )
end
Also aliased as: /
pattern() click to toggle source

Returns the pattern for this template.

@return String

# File lib/uri_template.rb, line 323
def pattern
  @pattern ||= tokens.map(&:to_s).join
end
Also aliased as: to_s
relative?() click to toggle source

Opposite of {#absolute?}

# File lib/uri_template.rb, line 447
def relative?
  !absolute?
end
scheme?() click to toggle source

Returns whether this uri-template includes a scheme

This method is usefull to check wheter this template will generate or match a uri with a scheme.

@see host?

@example

URITemplate.new('/foo').scheme? #=> false
URITemplate.new('//example.com/foo').scheme? #=> false
URITemplate.new('http://example.com/foo').scheme? #=> true
URITemplate.new('{scheme}://example.com/foo').scheme? #=> true
# File lib/uri_template.rb, line 316
def scheme?
  return scheme_and_host[0]
end
static_characters() click to toggle source

Returns the number of static characters in this template. This method is useful for routing, since it's often pointful to use the url with fewer variable characters. For example 'static' and 'sta\{var\}' both match 'static', but in most cases 'static' should be prefered over 'sta\{var\}' since it's more specific.

@example

URITemplate.new('/xy/').static_characters #=> 4
URITemplate.new('{foo}').static_characters #=> 0
URITemplate.new('a{foo}b').static_characters #=> 2

@return Numeric

# File lib/uri_template.rb, line 281
def static_characters
  @static_characters ||= tokens.select(&:literal?).map{|t| t.string.size }.inject(0,:+)
end
to_s()
Alias for: pattern
tokens() click to toggle source

@abstract Returns the tokens of this templates. Tokens should include either {Literal} or {Expression}.

@return [Array<URITemplate::Token>]

# File lib/uri_template.rb, line 257
def tokens
  raise "Please implement #tokens on #{self.class.inspect}."
end
type() click to toggle source

@abstract Returns the type of this template. The type is a symbol which can be used in {.resolve_class} to resolve the type of this template.

@return [Symbol]

# File lib/uri_template.rb, line 249
def type
  raise "Please implement #type on #{self.class.inspect}."
end
variables() click to toggle source

Returns an array containing all variables. Repeated variables are ignored. The concrete order of the variables may change. @example

URITemplate.new('{foo}{bar}{baz}').variables #=> ['foo','bar','baz']
URITemplate.new('{a}{c}{a}{b}').variables #=> ['a','c','b']

@return [Array<String>]

# File lib/uri_template.rb, line 267
def variables
  @variables ||= tokens.map(&:variables).flatten.uniq.freeze
end

Private Instance Methods

normalize_variables( variables ) click to toggle source
# File lib/uri_template.rb, line 229
def normalize_variables( variables )
  raise ArgumentError, "Expected something that responds to :map, but got: #{variables.inspect}" unless variables.respond_to? :map
  if variables.kind_of?(Array)
    return Hash[self.variables.zip(variables)]
  else
    # Stringify variables
    arg = variables.map{ |k, v| [k.to_s, v] }
    if arg.any?{|elem| !elem.kind_of?(Array) }
      raise ArgumentError, "Expected the output of variables.map to return an array of arrays but got #{arg.inspect}"
    end
    return Hash[arg]
  end
end
remove_double_slash( first_tokens, second_tokens ) click to toggle source

@api private

# File lib/uri_template.rb, line 399
def remove_double_slash( first_tokens, second_tokens )
  if first_tokens.last.literal?
    return first_tokens[0..-2] + [ first_tokens.last.to_s[0..-2] ] + second_tokens
  elsif second_tokens.first.literal?
    return first_tokens + [ second_tokens.first.to_s[1..-1] ] + second_tokens[1..-1]
  else
    raise ArgumentError, "Cannot remove double slashes from #{first_tokens.inspect} and #{second_tokens.inspect}."
  end
end
scheme_and_host() click to toggle source

@api private

# File lib/uri_template.rb, line 412
def scheme_and_host
  return @scheme_and_host if @scheme_and_host
  read_chars = ""
  @scheme_and_host = [false,false]
  tokens.each do |token|
    if token.expression?
      read_chars << "x"
      if token.scheme?
        read_chars << ':'
      end
      if token.host?
        read_chars << '//'
      end
      read_chars << "x"
    elsif token.literal?
      read_chars << token.string
    end
    if read_chars =~ SCHEME_REGEX
      @scheme_and_host = [true, true]
      break
    elsif read_chars =~ HOST_REGEX
      @scheme_and_host[1] = true
      break
    elsif read_chars =~ /(^|[^:\/])\/(?!\/)/
      break
    end
  end
  return @scheme_and_host
end