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
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
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
@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
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
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
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
Compares two template patterns.
# File lib/uri_template.rb, line 330 def eq(other) return self.pattern == other.pattern end
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
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
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
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
Returns the pattern for this template.
@return String
# File lib/uri_template.rb, line 323 def pattern @pattern ||= tokens.map(&:to_s).join end
Opposite of {#absolute?}
# File lib/uri_template.rb, line 447 def relative? !absolute? end
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
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
@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
@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
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
# 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
@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
@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