uri_template 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +129 -0
- data/lib/uri_template.rb +110 -0
- data/lib/uri_template/draft7.rb +876 -0
- data/lib/uri_template/utils.rb +125 -0
- data/uri_template.gemspec +17 -0
- metadata +76 -0
data/README
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
= URITemplate - a uri template library
|
2
|
+
|
3
|
+
With URITemplate you can generate URI based on simple templates. The current implementation is based on draft 7 of the uri template spec ( http://tools.ietf.org/html/draft-gregorio-uritemplate-07 ). For the syntax read that spec. Draft 2 of that specification is implemented as addressable gem.
|
4
|
+
|
5
|
+
It's currently planed to add newer versions of that spec when they emerge. Therefore the module URITemplate just defines one abstract method ( expand ) and some methods to access the actual implementations.
|
6
|
+
|
7
|
+
Some implementations might be able to extract variables, too ( Draft 7 is! ).
|
8
|
+
|
9
|
+
== Examples
|
10
|
+
|
11
|
+
require 'uri_template'
|
12
|
+
|
13
|
+
tpl = URITemplate.new('http://{host}{/segments*}/{file}{.extensions*}')
|
14
|
+
|
15
|
+
# This will give: http://www.host.com/path/to/a/file.x.y
|
16
|
+
tpl.expand('host'=>'www.host.com','segments'=>['path','to','a'],'file'=>'file','extensions'=>['x','y'])
|
17
|
+
|
18
|
+
# This will give: { 'host'=>'www.host.com','segments'=>['path','to','a'],'file'=>'file','extensions'=>['x','y']}
|
19
|
+
tpl.extract('http://www.host.com/path/to/a/file.x.y')
|
20
|
+
|
21
|
+
|
22
|
+
== Benchmarks
|
23
|
+
|
24
|
+
System: Core 2 Duo T9600, 4 gb ram, Fedora 15 64 bit, 10_000 repetitions
|
25
|
+
Implementation: Draft7
|
26
|
+
Results marked with * means that the template object is reused.
|
27
|
+
|
28
|
+
=== Expansion
|
29
|
+
"" vs "" => ""
|
30
|
+
user system total real
|
31
|
+
Addressable 0.070000 0.010000 0.080000 ( 0.081990)
|
32
|
+
UriTemplate 0.010000 0.000000 0.010000 ( 0.007668)
|
33
|
+
Addressable* 0.090000 0.000000 0.090000 ( 0.090763)
|
34
|
+
UriTemplate* 0.000000 0.000000 0.000000 ( 0.001633)
|
35
|
+
|
36
|
+
"{simple_string}" vs "{simple_string}" => "noneedtoescape"
|
37
|
+
user system total real
|
38
|
+
Addressable 0.560000 0.000000 0.560000 ( 0.560811)
|
39
|
+
UriTemplate 0.030000 0.000000 0.030000 ( 0.034104)
|
40
|
+
Addressable* 0.560000 0.000000 0.560000 ( 0.558118)
|
41
|
+
UriTemplate* 0.030000 0.000000 0.030000 ( 0.026850)
|
42
|
+
|
43
|
+
"{escaped_string}" vs "{escaped_string}" => "%2F%20%2F%25%2F%20%3F%2B"
|
44
|
+
user system total real
|
45
|
+
Addressable 0.560000 0.000000 0.560000 ( 0.562588)
|
46
|
+
UriTemplate 0.070000 0.000000 0.070000 ( 0.072649)
|
47
|
+
Addressable* 0.560000 0.000000 0.560000 ( 0.556001)
|
48
|
+
UriTemplate* 0.050000 0.000000 0.050000 ( 0.054520)
|
49
|
+
|
50
|
+
"{missing}" vs "{missing}" => ""
|
51
|
+
user system total real
|
52
|
+
Addressable 0.550000 0.010000 0.560000 ( 0.550707)
|
53
|
+
UriTemplate 0.040000 0.000000 0.040000 ( 0.035343)
|
54
|
+
Addressable* 0.550000 0.000000 0.550000 ( 0.556634)
|
55
|
+
UriTemplate* 0.010000 0.000000 0.010000 ( 0.003132)
|
56
|
+
|
57
|
+
"{-prefix|/|segments}" vs "{/segments*}" => "/a/b/c"
|
58
|
+
user system total real
|
59
|
+
Addressable 0.650000 0.000000 0.650000 ( 0.656996)
|
60
|
+
UriTemplate 0.030000 0.000000 0.030000 ( 0.038546)
|
61
|
+
Addressable* 0.660000 0.000000 0.660000 ( 0.655364)
|
62
|
+
UriTemplate* 0.020000 0.000000 0.020000 ( 0.018265)
|
63
|
+
|
64
|
+
"?{-join|&|one,two,three}" vs "{?one,two,three}" => "?one=1&two=2&three=3"
|
65
|
+
user system total real
|
66
|
+
Addressable 0.670000 0.000000 0.670000 ( 0.671649)
|
67
|
+
UriTemplate 0.080000 0.000000 0.080000 ( 0.078588)
|
68
|
+
Addressable* 0.670000 0.000000 0.670000 ( 0.669606)
|
69
|
+
UriTemplate* 0.030000 0.000000 0.030000 ( 0.035886)
|
70
|
+
|
71
|
+
"http://{host}/{-suffix|/|segments}?{-join|&|one,two,bogus}\#{fragment}" vs "http://{host}{/segments*}/{?one,two,bogus}{#fragment}" => "http://example.com/a/b/c/?one=1&two=2#foo"
|
72
|
+
user system total real
|
73
|
+
Addressable 0.800000 0.000000 0.800000 ( 0.802170)
|
74
|
+
UriTemplate 0.150000 0.000000 0.150000 ( 0.147875)
|
75
|
+
Addressable* 0.810000 0.000000 0.810000 ( 0.803928)
|
76
|
+
UriTemplate* 0.060000 0.000000 0.060000 ( 0.064376)
|
77
|
+
|
78
|
+
=== Extraction
|
79
|
+
"" => "" vs ""
|
80
|
+
user system total real
|
81
|
+
Addressable 0.150000 0.000000 0.150000 ( 0.144244)
|
82
|
+
UriTemplate 0.080000 0.000000 0.080000 ( 0.079467)
|
83
|
+
Addressable* 0.140000 0.000000 0.140000 ( 0.149177)
|
84
|
+
UriTemplate* 0.010000 0.000000 0.010000 ( 0.008808)
|
85
|
+
|
86
|
+
"noneedtoescape" => "{simple_string}" vs "{simple_string}"
|
87
|
+
user system total real
|
88
|
+
Addressable 0.350000 0.000000 0.350000 ( 0.345314)
|
89
|
+
UriTemplate 0.110000 0.000000 0.110000 ( 0.116090)
|
90
|
+
Addressable* 0.350000 0.000000 0.350000 ( 0.342613)
|
91
|
+
UriTemplate* 0.020000 0.000000 0.020000 ( 0.025884)
|
92
|
+
|
93
|
+
"%2F%20%2F%25%2F%20%3F%2B" => "{escaped_string}" vs "{escaped_string}"
|
94
|
+
user system total real
|
95
|
+
Addressable 0.410000 0.000000 0.410000 ( 0.404258)
|
96
|
+
UriTemplate 0.140000 0.000000 0.140000 ( 0.143897)
|
97
|
+
Addressable* 0.380000 0.000000 0.380000 ( 0.382331)
|
98
|
+
UriTemplate* 0.060000 0.000000 0.060000 ( 0.053483)
|
99
|
+
|
100
|
+
"" => "{missing}" vs "{missing}"
|
101
|
+
user system total real
|
102
|
+
Addressable 0.220000 0.000000 0.220000 ( 0.222086)
|
103
|
+
UriTemplate 0.100000 0.000000 0.100000 ( 0.104280)
|
104
|
+
Addressable* 0.230000 0.000000 0.230000 ( 0.238814)
|
105
|
+
UriTemplate* 0.020000 0.000000 0.020000 ( 0.017284)
|
106
|
+
|
107
|
+
"/a/b/c" => "{-prefix|/|segments}" vs "{/segments*}"
|
108
|
+
user system total real
|
109
|
+
Addressable 0.490000 0.000000 0.490000 ( 0.489809)
|
110
|
+
UriTemplate 0.230000 0.000000 0.230000 ( 0.232298)
|
111
|
+
Addressable* 0.490000 0.000000 0.490000 ( 0.489334)
|
112
|
+
UriTemplate* 0.120000 0.000000 0.120000 ( 0.119815)
|
113
|
+
|
114
|
+
"?one=1&two=2&three=3" => "?{-join|&|one,two,three}" vs "{?one,two,three}"
|
115
|
+
user system total real
|
116
|
+
Addressable 0.550000 0.000000 0.550000 ( 0.553224)
|
117
|
+
UriTemplate 0.210000 0.000000 0.210000 ( 0.208296)
|
118
|
+
Addressable* 0.550000 0.000000 0.550000 ( 0.551459)
|
119
|
+
UriTemplate* 0.060000 0.000000 0.060000 ( 0.059963)
|
120
|
+
|
121
|
+
"http://example.com/a/b/c/?one=1&two=2#foo" => "http://{host}/{-suffix|/|segments}?{-join|&|one,two,bogus}\#{fragment}" vs "http://{host}{/segments*}/{?one,two,bogus}{#fragment}"
|
122
|
+
user system total real
|
123
|
+
Addressable 0.980000 0.000000 0.980000 ( 0.980292)
|
124
|
+
UriTemplate 0.450000 0.000000 0.450000 ( 0.446143)
|
125
|
+
Addressable* 1.000000 0.000000 1.000000 ( 0.996237)
|
126
|
+
UriTemplate* 0.170000 0.000000 0.170000 ( 0.175239)
|
127
|
+
|
128
|
+
SUCCESS - Draft7 was faster in every test!
|
129
|
+
|
data/lib/uri_template.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# This program is free software: you can redistribute it and/or modify
|
3
|
+
# it under the terms of the Affero GNU General Public License as published by
|
4
|
+
# the Free Software Foundation, either version 3 of the License, or
|
5
|
+
# (at your option) any later version.
|
6
|
+
#
|
7
|
+
# This program is distributed in the hope that it will be useful,
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
10
|
+
# GNU General Public License for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License
|
13
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
14
|
+
#
|
15
|
+
# (c) 2011 by Hannes Georg
|
16
|
+
#
|
17
|
+
|
18
|
+
# A base module for all implementations of a uri template.
|
19
|
+
module URITemplate
|
20
|
+
|
21
|
+
autoload :Draft7, 'uri_template/draft7'
|
22
|
+
|
23
|
+
# A hash with all available implementations.
|
24
|
+
# Currently the only implementation is :draft7. But there also aliases :default and :latest available. This should make it possible to add newer specs later.
|
25
|
+
# @see resolve_class
|
26
|
+
VERSIONS = {
|
27
|
+
:draft7 => :Draft7,
|
28
|
+
:default => :Draft7,
|
29
|
+
:latest => :Draft7
|
30
|
+
}
|
31
|
+
|
32
|
+
# Looks up which implementation to use.
|
33
|
+
# Extracts all symbols from args and looks up the first in {VERSIONS}.
|
34
|
+
#
|
35
|
+
# @return Array an array of the class to use and the unused parameters.
|
36
|
+
# @example
|
37
|
+
# URITemplate.resolve_class() #=> [ URITemplate::Draft7, [] ]
|
38
|
+
# URITemplate.resolve_class(:draft7) #=> [ URITemplate::Draft7, [] ]
|
39
|
+
# URITemplate.resolve_class("template",:draft7) #=> [ URITemplate::Draft7, ["template"] ]
|
40
|
+
#
|
41
|
+
# @raise ArgumentError when no class was found.
|
42
|
+
def self.resolve_class(*args)
|
43
|
+
symbols, rest = args.partition{|x| x.kind_of? Symbol }
|
44
|
+
version = symbols.fetch(0, :default)
|
45
|
+
raise ArgumentError, "Unknown template version #{version.inspect}, defined versions: #{VERSIONS.keys.inspect}" unless VERSIONS.key?(version)
|
46
|
+
return self.const_get(VERSIONS[version]), rest
|
47
|
+
end
|
48
|
+
|
49
|
+
# Creates an uri template using an implementation.
|
50
|
+
# The args should at least contain a pattern string.
|
51
|
+
# Symbols in the args are used to determine the actual implementation.
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# tpl = URITemplate.new('{x}') # a new template using the default implementation
|
55
|
+
# tpl.expand('x'=>'y') #=> 'y'
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# tpl = URITemplate.new(:draft7,'{x}') # a new template using the draft7 implementation
|
59
|
+
#
|
60
|
+
def self.new(*args)
|
61
|
+
klass, rest = resolve_class(*args)
|
62
|
+
return klass.new(*rest)
|
63
|
+
end
|
64
|
+
|
65
|
+
# A base class for all errors which will be raised upon invalid syntax.
|
66
|
+
module Invalid
|
67
|
+
end
|
68
|
+
|
69
|
+
# A base module for all implementation of a template section.
|
70
|
+
# Sections are a custom extension to the uri template spec.
|
71
|
+
# A template section ( in comparison to a template ) can be unbounded on its ends. Therefore they don't necessarily match a whole uri and can be concatenated.
|
72
|
+
module Section
|
73
|
+
|
74
|
+
include URITemplate
|
75
|
+
|
76
|
+
# Same as {URITemplate.new} but for sections
|
77
|
+
def self.new(*args)
|
78
|
+
klass, rest = URITemplate.resolve_class(*args)
|
79
|
+
return klass::Section.new(*rest)
|
80
|
+
end
|
81
|
+
|
82
|
+
# @abstract
|
83
|
+
# Concatenates this section with an other section.
|
84
|
+
def >>(other)
|
85
|
+
raise "Please implement #>> on #{self.class.inspect}"
|
86
|
+
end
|
87
|
+
|
88
|
+
# @abstract
|
89
|
+
# Is this section left bounded?
|
90
|
+
def left_bound?
|
91
|
+
raise "Please implement #left_bound? on #{self.class.inspect}"
|
92
|
+
end
|
93
|
+
|
94
|
+
# @abstract
|
95
|
+
# Is this section right bounded?
|
96
|
+
def right_bound?
|
97
|
+
raise "Please implement #right_bound? on #{self.class.inspect}"
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
# @abstract
|
103
|
+
# Expands this uri template with the given variables.
|
104
|
+
# The variables should be converted to strings using {Utils#object_to_param}.
|
105
|
+
# @raise Unconvertable if a variable could not be converted.
|
106
|
+
def expand(variables={})
|
107
|
+
raise "Please implement #expand on #{self.class.inspect}"
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,876 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# This program is free software: you can redistribute it and/or modify
|
3
|
+
# it under the terms of the Affero GNU General Public License as published by
|
4
|
+
# the Free Software Foundation, either version 3 of the License, or
|
5
|
+
# (at your option) any later version.
|
6
|
+
#
|
7
|
+
# This program is distributed in the hope that it will be useful,
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
10
|
+
# GNU General Public License for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License
|
13
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
14
|
+
#
|
15
|
+
# (c) 2011 by Hannes Georg
|
16
|
+
#
|
17
|
+
|
18
|
+
require 'strscan'
|
19
|
+
require 'set'
|
20
|
+
require 'forwardable'
|
21
|
+
|
22
|
+
require 'uri_template'
|
23
|
+
require 'uri_template/utils'
|
24
|
+
|
25
|
+
# A uri template which should comply with the uri template spec draft 7 ( http://tools.ietf.org/html/draft-gregorio-uritemplate-07 ).
|
26
|
+
# @note
|
27
|
+
# Most specs and examples refer to this class directly, because they are acutally refering to this specific implementation. If you just want uri templates, you should rather use the methods on {URITemplate} to create templates since they will select an implementation.
|
28
|
+
class URITemplate::Draft7
|
29
|
+
|
30
|
+
include URITemplate
|
31
|
+
extend Forwardable
|
32
|
+
|
33
|
+
# @private
|
34
|
+
Utils = URITemplate::Utils
|
35
|
+
|
36
|
+
# @private
|
37
|
+
LITERAL = /^([^"'%<>\\^`{|}\s]|%\h\h)+/
|
38
|
+
|
39
|
+
# @private
|
40
|
+
CHARACTER_CLASSES = {
|
41
|
+
|
42
|
+
:unreserved => {
|
43
|
+
:unencoded => /([^A-Za-z0-9\-\._])/,
|
44
|
+
:class => '(?<c_u_>[A-Za-z0-9\-\._]|%\h\h)',
|
45
|
+
:class_name => 'c_u_',
|
46
|
+
:grabs_comma => false
|
47
|
+
},
|
48
|
+
:unreserved_reserved_pct => {
|
49
|
+
:unencoded => /([^A-Za-z0-9\-\._:\/?#\[\]@!\$%'\(\)*+,;=]|%(?!\h\h))/,
|
50
|
+
:class => '(?<c_urp_>[A-Za-z0-9\-\._:\/?#\[\]@!\$%\'\(\)*+,;=]|%\h\h)',
|
51
|
+
:class_name => 'c_urp_',
|
52
|
+
:grabs_comma => true
|
53
|
+
},
|
54
|
+
|
55
|
+
:varname => {
|
56
|
+
:class => '(?<c_vn_> (?:[a-zA-Z_]|%[0-9a-fA-F]{2})(?:[a-zA-Z_\.]|%[0-9a-fA-F]{2})*?)',
|
57
|
+
:class_name => 'c_vn_'
|
58
|
+
}
|
59
|
+
|
60
|
+
}
|
61
|
+
|
62
|
+
# Specifies that no processing should be done upon extraction.
|
63
|
+
# @see extract
|
64
|
+
NO_PROCESSING = []
|
65
|
+
|
66
|
+
# Specifies that the extracted values should be processed.
|
67
|
+
# @see extract
|
68
|
+
CONVERT_VALUES = [:convert_values]
|
69
|
+
|
70
|
+
# Specifies that the extracted variable list should be processed.
|
71
|
+
# @see extract
|
72
|
+
CONVERT_RESULT = [:convert_result]
|
73
|
+
|
74
|
+
# Default processing. Means: convert values and the list itself.
|
75
|
+
# @see extract
|
76
|
+
DEFAULT_PROCESSING = CONVERT_VALUES + CONVERT_RESULT
|
77
|
+
|
78
|
+
# @private
|
79
|
+
VAR = Regexp.compile(<<'__REGEXP__'.strip, Regexp::EXTENDED)
|
80
|
+
(?<operator> [+#\./;?&]?){0}
|
81
|
+
(?<varchar> [a-zA-Z_]|%[0-9a-fA-F]{2}){0}
|
82
|
+
(?<varname> \g<varchar>(?:\g<varchar>|\.)*){0}
|
83
|
+
(?<varspec> \g<varname>(?<explode>\*?)(?::(?<length>\d+))?){0}
|
84
|
+
\g<varspec>
|
85
|
+
__REGEXP__
|
86
|
+
|
87
|
+
# @private
|
88
|
+
EXPRESSION = Regexp.compile(<<'__REGEXP__'.strip, Regexp::EXTENDED)
|
89
|
+
(?<operator> [+#\./;?&]?){0}
|
90
|
+
(?<varchar> [a-zA-Z_]|%[0-9a-fA-F]{2}){0}
|
91
|
+
(?<varname> \g<varchar>(?:\g<varchar>|\.)*){0}
|
92
|
+
(?<varspec> \g<varname>\*?(?::\d+)?){0}
|
93
|
+
\{\g<operator>(?<vars>\g<varspec>(?:,\g<varspec>)*)\}
|
94
|
+
__REGEXP__
|
95
|
+
|
96
|
+
# @private
|
97
|
+
URI = Regexp.compile(<<'__REGEXP__'.strip, Regexp::EXTENDED)
|
98
|
+
(?<operator> [+#\./;?&]?){0}
|
99
|
+
(?<varchar> [a-zA-Z_]|%[0-9a-fA-F]{2}){0}
|
100
|
+
(?<varname> \g<varchar>(?:\g<varchar>|\.)*){0}
|
101
|
+
(?<varspec> \g<varname>\*?(?::\d+)?){0}
|
102
|
+
^(([^"'%<>^`{|}\s]|%\h\h)+|\{\g<operator>(?<vars>\g<varspec>(?:,\g<varspec>)*)\})*$
|
103
|
+
__REGEXP__
|
104
|
+
|
105
|
+
# @private
|
106
|
+
class Literal
|
107
|
+
|
108
|
+
attr_reader :string
|
109
|
+
|
110
|
+
def initialize(string)
|
111
|
+
@string = string
|
112
|
+
end
|
113
|
+
|
114
|
+
def size
|
115
|
+
0
|
116
|
+
end
|
117
|
+
|
118
|
+
def expand(*_)
|
119
|
+
return @string
|
120
|
+
end
|
121
|
+
|
122
|
+
def to_r_source(*_)
|
123
|
+
Regexp.escape(@string)
|
124
|
+
end
|
125
|
+
|
126
|
+
def to_s
|
127
|
+
@string
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
# @private
|
133
|
+
class LeftBound
|
134
|
+
|
135
|
+
def expand(*_)
|
136
|
+
''
|
137
|
+
end
|
138
|
+
|
139
|
+
def to_r_source(*_)
|
140
|
+
'^'
|
141
|
+
end
|
142
|
+
|
143
|
+
def size
|
144
|
+
0
|
145
|
+
end
|
146
|
+
|
147
|
+
def to_s
|
148
|
+
''
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
# @private
|
154
|
+
class RightBound
|
155
|
+
|
156
|
+
def expand(*_)
|
157
|
+
''
|
158
|
+
end
|
159
|
+
|
160
|
+
def to_r_source(*_)
|
161
|
+
'$'
|
162
|
+
end
|
163
|
+
|
164
|
+
def size
|
165
|
+
0
|
166
|
+
end
|
167
|
+
|
168
|
+
def to_s
|
169
|
+
''
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
174
|
+
# @private
|
175
|
+
class Open
|
176
|
+
def expand(*_)
|
177
|
+
''
|
178
|
+
end
|
179
|
+
def to_r_source(*_)
|
180
|
+
''
|
181
|
+
end
|
182
|
+
def size
|
183
|
+
0
|
184
|
+
end
|
185
|
+
def to_s
|
186
|
+
"\u2026"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# @private
|
191
|
+
class Expression
|
192
|
+
|
193
|
+
attr_reader :variables, :max_length
|
194
|
+
|
195
|
+
def initialize(vars)
|
196
|
+
@variables = vars
|
197
|
+
end
|
198
|
+
|
199
|
+
PREFIX = ''.freeze
|
200
|
+
SEPARATOR = ','.freeze
|
201
|
+
PAIR_CONNECTOR = '='.freeze
|
202
|
+
PAIR_IF_EMPTY = true
|
203
|
+
LIST_CONNECTOR = ','.freeze
|
204
|
+
|
205
|
+
CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved]
|
206
|
+
|
207
|
+
NAMED = false
|
208
|
+
OPERATOR = ''
|
209
|
+
|
210
|
+
def size
|
211
|
+
@variables.size
|
212
|
+
end
|
213
|
+
|
214
|
+
def expand( vars, options )
|
215
|
+
result = []
|
216
|
+
variables.each{| var, expand , max_length |
|
217
|
+
unless vars[var].nil?
|
218
|
+
if vars[var].kind_of? Hash
|
219
|
+
result.push( *transform_hash(var, vars[var], expand, max_length) )
|
220
|
+
elsif vars[var].kind_of? Array
|
221
|
+
result.push( *transform_array(var, vars[var], expand, max_length) )
|
222
|
+
else
|
223
|
+
if self.class::NAMED
|
224
|
+
result.push( pair(var, vars[var], max_length) )
|
225
|
+
else
|
226
|
+
result.push( cut( encode(vars[var]), max_length ) )
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
}
|
231
|
+
if result.any?
|
232
|
+
return (self.class::PREFIX + result.join(self.class::SEPARATOR))
|
233
|
+
else
|
234
|
+
return ''
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def to_s
|
239
|
+
'{' + self.class::OPERATOR + @variables.map{|name,expand,max_length| name +(expand ? '*': '') + (max_length > 0 ? ':'+max_length.to_s : '') }.join(',') + '}'
|
240
|
+
end
|
241
|
+
|
242
|
+
#TODO: certain things after a slurpy variable will never get matched. therefore, it's pointless to add expressions for them
|
243
|
+
#TODO: variables, which appear twice could be compacted, don't they?
|
244
|
+
def to_r_source(base_counter = 0)
|
245
|
+
source = []
|
246
|
+
first = true
|
247
|
+
vs = variables.size - 1
|
248
|
+
i = 0
|
249
|
+
if self.class::NAMED
|
250
|
+
variables.each{| var, expand , max_length |
|
251
|
+
last = (vs == i)
|
252
|
+
value = "(?:\\g<#{self.class::CHARACTER_CLASS[:class_name]}>|,)#{(max_length > 0)?'{,'+max_length.to_s+'}':'*'}"
|
253
|
+
if expand
|
254
|
+
#if self.class::PAIR_IF_EMPTY
|
255
|
+
pair = "\\g<c_vn_>(?:#{Regexp.escape(self.class::PAIR_CONNECTOR)}#{value})?"
|
256
|
+
|
257
|
+
if first
|
258
|
+
source << "(?<v#{base_counter + i}>(?:#{pair})(?:#{Regexp.escape(self.class::SEPARATOR)}#{pair})*)"
|
259
|
+
else
|
260
|
+
source << "(?<v#{base_counter + i}>(?:#{Regexp.escape(self.class::SEPARATOR)}#{pair})*)"
|
261
|
+
end
|
262
|
+
else
|
263
|
+
if self.class::PAIR_IF_EMPTY
|
264
|
+
pair = "#{Regexp.escape(var)}(?<v#{base_counter + i}>#{Regexp.escape(self.class::PAIR_CONNECTOR)}#{value})?"
|
265
|
+
else
|
266
|
+
pair = "#{Regexp.escape(var)}(?<v#{base_counter + i}>#{Regexp.escape(self.class::PAIR_CONNECTOR)}#{value}|)"
|
267
|
+
end
|
268
|
+
|
269
|
+
if first
|
270
|
+
source << "(?:#{pair})"
|
271
|
+
else
|
272
|
+
source << "(?:#{Regexp.escape(self.class::SEPARATOR)}#{pair})?"
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
first = false
|
277
|
+
i = i+1
|
278
|
+
}
|
279
|
+
else
|
280
|
+
variables.each{| var, expand , max_length |
|
281
|
+
last = (vs == i)
|
282
|
+
if expand
|
283
|
+
# could be list or map, too
|
284
|
+
value = "\\g<#{self.class::CHARACTER_CLASS[:class_name]}>#{(max_length > 0)?'{,'+max_length.to_s+'}':'*'}"
|
285
|
+
|
286
|
+
pair = "\\g<c_vn_>(?:#{Regexp.escape(self.class::PAIR_CONNECTOR)}#{value})?"
|
287
|
+
|
288
|
+
value = "#{pair}(?:#{Regexp.escape(self.class::SEPARATOR)}#{pair})*"
|
289
|
+
elsif last
|
290
|
+
# the last will slurp lists
|
291
|
+
if self.class::CHARACTER_CLASS[:grabs_comma]
|
292
|
+
value = "(?:\\g<#{self.class::CHARACTER_CLASS[:class_name]}>)#{(max_length > 0)?'{,'+max_length.to_s+'}':'*?'}"
|
293
|
+
else
|
294
|
+
value = "(?:\\g<#{self.class::CHARACTER_CLASS[:class_name]}>|,)#{(max_length > 0)?'{,'+max_length.to_s+'}':'*?'}"
|
295
|
+
end
|
296
|
+
else
|
297
|
+
value = "\\g<#{self.class::CHARACTER_CLASS[:class_name]}>#{(max_length > 0)?'{,'+max_length.to_s+'}':'*?'}"
|
298
|
+
end
|
299
|
+
if first
|
300
|
+
source << "(?<v#{base_counter + i}>#{value})"
|
301
|
+
first = false
|
302
|
+
else
|
303
|
+
source << "(?:#{Regexp.escape(self.class::SEPARATOR)}(?<v#{base_counter + i}>#{value}))?"
|
304
|
+
end
|
305
|
+
i = i+1
|
306
|
+
}
|
307
|
+
end
|
308
|
+
return '(?:' + Regexp.escape(self.class::PREFIX) + source.join + ')?'
|
309
|
+
end
|
310
|
+
|
311
|
+
def extract(position,matched)
|
312
|
+
name, expand, max_length = @variables[position]
|
313
|
+
if matched.nil?
|
314
|
+
return [[ name , matched ]]
|
315
|
+
end
|
316
|
+
if expand
|
317
|
+
ex = self.hash_extractor(max_length)
|
318
|
+
rest = matched
|
319
|
+
splitted = []
|
320
|
+
found_value = false
|
321
|
+
until rest.size == 0
|
322
|
+
match = ex.match(rest)
|
323
|
+
if match.nil?
|
324
|
+
raise "Couldn't match #{rest.inspect} againts the hash extractor. This is definitly a Bug. Please report this ASAP!"
|
325
|
+
end
|
326
|
+
if match.post_match.size == 0
|
327
|
+
rest = match['rest'].to_s
|
328
|
+
else
|
329
|
+
rest = ''
|
330
|
+
end
|
331
|
+
if match['name']
|
332
|
+
found_value = true
|
333
|
+
splitted << [ match['name'][0..-2], decode(match['value'] + rest , false) ]
|
334
|
+
else
|
335
|
+
splitted << [ decode(match['value'] + rest , false), nil ]
|
336
|
+
end
|
337
|
+
rest = match.post_match
|
338
|
+
end
|
339
|
+
if !found_value
|
340
|
+
return [ [ name, splitted.map{|n,v| v || n } ] ]
|
341
|
+
else
|
342
|
+
return [ [ name, splitted ] ]
|
343
|
+
end
|
344
|
+
elsif self.class::NAMED
|
345
|
+
return [ [ name, decode( matched[1..-1] ) ] ]
|
346
|
+
end
|
347
|
+
|
348
|
+
return [ [ name, decode( matched ) ] ]
|
349
|
+
end
|
350
|
+
|
351
|
+
def variable_names
|
352
|
+
@variables.collect(&:first)
|
353
|
+
end
|
354
|
+
|
355
|
+
protected
|
356
|
+
|
357
|
+
def hash_extractor(max_length)
|
358
|
+
value = "\\g<#{self.class::CHARACTER_CLASS[:class_name]}>#{(max_length > 0)?'{,'+max_length.to_s+'}':'*?'}"
|
359
|
+
|
360
|
+
pair = "(?<name>\\g<c_vn_>#{Regexp.escape(self.class::PAIR_CONNECTOR)})?(?<value>#{value})"
|
361
|
+
|
362
|
+
return Regexp.new( CHARACTER_CLASSES[:varname][:class] + "{0}\n" + self.class::CHARACTER_CLASS[:class] + "{0}\n" + "^#{Regexp.escape(self.class::SEPARATOR)}?" + pair + "(?<rest>$|#{Regexp.escape(self.class::SEPARATOR)}(?!#{Regexp.escape(self.class::SEPARATOR)}))" ,Regexp::EXTENDED)
|
363
|
+
|
364
|
+
end
|
365
|
+
|
366
|
+
def encode(x)
|
367
|
+
Utils.pct(Utils.object_to_param(x), self.class::CHARACTER_CLASS[:unencoded])
|
368
|
+
end
|
369
|
+
|
370
|
+
SPLITTER = /^(?:,(,*)|([^,]+))/
|
371
|
+
|
372
|
+
def decode(x, split = true)
|
373
|
+
if x.nil?
|
374
|
+
if self.class::PAIR_IF_EMPTY
|
375
|
+
return x
|
376
|
+
else
|
377
|
+
return ''
|
378
|
+
end
|
379
|
+
elsif split
|
380
|
+
r = []
|
381
|
+
v = x
|
382
|
+
until v.size == 0
|
383
|
+
m = SPLITTER.match(v)
|
384
|
+
if m[1] and m[1].size > 0
|
385
|
+
r << m[1]
|
386
|
+
elsif m[2]
|
387
|
+
r << Utils.dpct(m[2])
|
388
|
+
end
|
389
|
+
v = m.post_match
|
390
|
+
end
|
391
|
+
case(r.size)
|
392
|
+
when 0 then ''
|
393
|
+
when 1 then r.first
|
394
|
+
else r
|
395
|
+
end
|
396
|
+
else
|
397
|
+
Utils.dpct(x)
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def cut(str,chars)
|
402
|
+
if chars > 0
|
403
|
+
md = Regexp.compile("^#{self.class::CHARACTER_CLASS[:class]}{,#{chars.to_s}}", Regexp::EXTENDED).match(str)
|
404
|
+
#TODO: handle invalid matches
|
405
|
+
return md[0]
|
406
|
+
else
|
407
|
+
return str
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
def pair(key, value, max_length = 0)
|
412
|
+
ek = encode(key)
|
413
|
+
ev = encode(value)
|
414
|
+
if !self.class::PAIR_IF_EMPTY and ev.size == 0
|
415
|
+
return ek
|
416
|
+
else
|
417
|
+
return ek + self.class::PAIR_CONNECTOR + cut( ev, max_length )
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
def transform_hash(name, hsh, expand , max_length)
|
422
|
+
if expand
|
423
|
+
hsh.map{|key,value| pair(key,value) }
|
424
|
+
elsif hsh.none?
|
425
|
+
[]
|
426
|
+
else
|
427
|
+
[ (self.class::NAMED ? encode(name)+self.class::PAIR_CONNECTOR : '' ) + hsh.map{|key,value| encode(key)+self.class::LIST_CONNECTOR+encode(value) }.join(self.class::LIST_CONNECTOR) ]
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
def transform_array(name, ary, expand , max_length)
|
432
|
+
if expand
|
433
|
+
ary.map{|value| encode(value) }
|
434
|
+
elsif ary.none?
|
435
|
+
[]
|
436
|
+
else
|
437
|
+
[ (self.class::NAMED ? encode(name)+self.class::PAIR_CONNECTOR : '' ) + ary.map{|value| encode(value) }.join(self.class::LIST_CONNECTOR) ]
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
class Reserved < self
|
442
|
+
|
443
|
+
CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved_reserved_pct]
|
444
|
+
OPERATOR = '+'.freeze
|
445
|
+
|
446
|
+
end
|
447
|
+
|
448
|
+
class Fragment < self
|
449
|
+
|
450
|
+
CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved_reserved_pct]
|
451
|
+
PREFIX = '#'.freeze
|
452
|
+
OPERATOR = '#'.freeze
|
453
|
+
|
454
|
+
end
|
455
|
+
|
456
|
+
class Label < self
|
457
|
+
|
458
|
+
SEPARATOR = '.'.freeze
|
459
|
+
PREFIX = '.'.freeze
|
460
|
+
OPERATOR = '.'.freeze
|
461
|
+
|
462
|
+
end
|
463
|
+
|
464
|
+
class Path < self
|
465
|
+
|
466
|
+
SEPARATOR = '/'.freeze
|
467
|
+
PREFIX = '/'.freeze
|
468
|
+
OPERATOR = '/'.freeze
|
469
|
+
|
470
|
+
end
|
471
|
+
|
472
|
+
class PathParameters < self
|
473
|
+
|
474
|
+
SEPARATOR = ';'.freeze
|
475
|
+
PREFIX = ';'.freeze
|
476
|
+
NAMED = true
|
477
|
+
PAIR_IF_EMPTY = false
|
478
|
+
OPERATOR = ';'.freeze
|
479
|
+
|
480
|
+
end
|
481
|
+
|
482
|
+
class FormQuery < self
|
483
|
+
|
484
|
+
SEPARATOR = '&'.freeze
|
485
|
+
PREFIX = '?'.freeze
|
486
|
+
NAMED = true
|
487
|
+
OPERATOR = '?'.freeze
|
488
|
+
|
489
|
+
end
|
490
|
+
|
491
|
+
class FormQueryContinuation < self
|
492
|
+
|
493
|
+
SEPARATOR = '&'.freeze
|
494
|
+
PREFIX = '&'.freeze
|
495
|
+
NAMED = true
|
496
|
+
OPERATOR = '&'.freeze
|
497
|
+
|
498
|
+
end
|
499
|
+
|
500
|
+
end
|
501
|
+
|
502
|
+
# @private
|
503
|
+
OPERATORS = {
|
504
|
+
'' => Expression,
|
505
|
+
'+' => Expression::Reserved,
|
506
|
+
'#' => Expression::Fragment,
|
507
|
+
'.' => Expression::Label,
|
508
|
+
'/' => Expression::Path,
|
509
|
+
';' => Expression::PathParameters,
|
510
|
+
'?' => Expression::FormQuery,
|
511
|
+
'&' => Expression::FormQueryContinuation
|
512
|
+
}
|
513
|
+
|
514
|
+
# This error is raised when an invalid pattern was given.
|
515
|
+
class Invalid < StandardError
|
516
|
+
|
517
|
+
include URITemplate::Invalid
|
518
|
+
|
519
|
+
attr_reader :pattern, :position
|
520
|
+
|
521
|
+
def initialize(source, position)
|
522
|
+
@pattern = pattern
|
523
|
+
@position = position
|
524
|
+
super("Invalid expression found in #{source.inspect} at #{position}: '#{source[position..-1]}'")
|
525
|
+
end
|
526
|
+
|
527
|
+
end
|
528
|
+
|
529
|
+
# @private
|
530
|
+
class Tokenizer
|
531
|
+
|
532
|
+
include Enumerable
|
533
|
+
|
534
|
+
attr_reader :source
|
535
|
+
|
536
|
+
def initialize(source)
|
537
|
+
@source = source
|
538
|
+
end
|
539
|
+
|
540
|
+
def each
|
541
|
+
if !block_given?
|
542
|
+
return Enumerator.new(self)
|
543
|
+
end
|
544
|
+
scanner = StringScanner.new(@source)
|
545
|
+
until scanner.eos?
|
546
|
+
expression = scanner.scan(EXPRESSION)
|
547
|
+
if expression
|
548
|
+
vars = scanner[5].split(',').map{|name|
|
549
|
+
match = VAR.match(name)
|
550
|
+
[ match['varname'], match['explode'] == '*', match['length'].to_i ]
|
551
|
+
}
|
552
|
+
yield OPERATORS[scanner[1]].new(vars)
|
553
|
+
else
|
554
|
+
literal = scanner.scan(LITERAL)
|
555
|
+
if literal
|
556
|
+
yield(Literal.new(literal))
|
557
|
+
else
|
558
|
+
raise Invalid.new(@source,scanner.pos)
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
end
|
565
|
+
|
566
|
+
# The class methods for all draft7 templates.
|
567
|
+
module ClassMethods
|
568
|
+
|
569
|
+
# Tries to convert the given param in to a instance of {Draft7}
|
570
|
+
# It basically passes thru instances of that class, parses strings and return nil on everything else.
|
571
|
+
# @example
|
572
|
+
# URITemplate::Draft7.try_convert( Object.new ) #=> nil
|
573
|
+
# tpl = URITemplate::Draft7.new('{foo}')
|
574
|
+
# URITemplate::Draft7.try_convert( tpl ) #=> tpl
|
575
|
+
# URITemplate::Draft7.try_convert('{foo}') #=> tpl
|
576
|
+
# # This pattern is invalid, so it wont be parsed:
|
577
|
+
# URITemplate::Draft7.try_convert('{foo') #=> nil
|
578
|
+
def try_convert(x)
|
579
|
+
if x.kind_of? self
|
580
|
+
return x
|
581
|
+
elsif x.kind_of? String and valid? x
|
582
|
+
return new(x)
|
583
|
+
else
|
584
|
+
return nil
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
# Tests whether a given pattern is a valid template pattern.
|
589
|
+
# @example
|
590
|
+
# URITemplate::Draft7.valid? 'foo' #=> true
|
591
|
+
# URITemplate::Draft7.valid? '{foo}' #=> true
|
592
|
+
# URITemplate::Draft7.valid? '{foo' #=> false
|
593
|
+
def valid?(pattern)
|
594
|
+
URI === pattern
|
595
|
+
end
|
596
|
+
|
597
|
+
end
|
598
|
+
|
599
|
+
extend ClassMethods
|
600
|
+
|
601
|
+
attr_reader :pattern
|
602
|
+
|
603
|
+
attr_reader :options
|
604
|
+
|
605
|
+
# @param String,Array either a pattern as String or an Array of tokens
|
606
|
+
# @param Hash some options
|
607
|
+
# @option :lazy If true the pattern will be parsed on first access, this also means that syntax errors will not be detected unless accessed.
|
608
|
+
def initialize(pattern_or_tokens,options={})
|
609
|
+
@options = options.dup.freeze
|
610
|
+
if pattern_or_tokens.kind_of? String
|
611
|
+
@pattern = pattern_or_tokens.dup
|
612
|
+
@pattern.freeze
|
613
|
+
unless @options[:lazy]
|
614
|
+
self.tokens
|
615
|
+
end
|
616
|
+
elsif pattern_or_tokens.kind_of? Array
|
617
|
+
@tokens = pattern_or_tokens.dup
|
618
|
+
@tokens.freeze
|
619
|
+
else
|
620
|
+
raise ArgumentError, "Expected to receive a pattern string, but got #{pattern_or_tokens.inspect}"
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
# Expands the template with the given variables.
|
625
|
+
# The expansion should be compatible to uritemplate spec draft 7 ( http://tools.ietf.org/html/draft-gregorio-uritemplate-07 ).
|
626
|
+
# @note
|
627
|
+
# All keys of the supplied hash should be strings as anything else won't be recognised.
|
628
|
+
# @note
|
629
|
+
# There are neither default values for variables nor will anything be raised if a variable is missing. Please read the spec if you want to know how undefined variables are handled.
|
630
|
+
# @example
|
631
|
+
# URITemplate::Draft7.new('{foo}').expand('foo'=>'bar') #=> 'bar'
|
632
|
+
# URITemplate::Draft7.new('{?args*}').expand('args'=>{'key'=>'value'}) #=> '?key=value'
|
633
|
+
# URITemplate::Draft7.new('{undef}').expand() #=> ''
|
634
|
+
#
|
635
|
+
# @param variables Hash
|
636
|
+
# @return String
|
637
|
+
def expand(variables = {})
|
638
|
+
tokens.map{|part|
|
639
|
+
part.expand(variables, {})
|
640
|
+
}.join
|
641
|
+
end
|
642
|
+
|
643
|
+
# Returns an array containing all variables. Repeated variables are ignored, but the order will be kept intact.
|
644
|
+
# @example
|
645
|
+
# URITemplate::Draft7.new('{foo}{bar}{baz}').variables #=> ['foo','bar','baz']
|
646
|
+
# URITemplate::Draft7.new('{a}{c}{a}{b}').variables #=> ['c','a','b']
|
647
|
+
#
|
648
|
+
# @return Array
|
649
|
+
def variables
|
650
|
+
@variables ||= begin
|
651
|
+
vars = []
|
652
|
+
tokens.each{|token|
|
653
|
+
if token.respond_to? :variable_names
|
654
|
+
vn = token.variable_names.uniq
|
655
|
+
vars -= vn
|
656
|
+
vars.push(*vn)
|
657
|
+
end
|
658
|
+
}
|
659
|
+
vars
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
# Compiles this template into a regular expression which can be used to test whether a given uri matches this template. This template is also used for {#===}.
|
664
|
+
#
|
665
|
+
# @example
|
666
|
+
# tpl = URITemplate::Draft7.new('/foo/{bar}/')
|
667
|
+
# regex = tpl.to_r
|
668
|
+
# regex === '/foo/baz/' #=> true
|
669
|
+
# regex === '/foz/baz/' #=> false
|
670
|
+
#
|
671
|
+
# @return Regexp
|
672
|
+
def to_r
|
673
|
+
classes = CHARACTER_CLASSES.map{|_,v| v[:class]+"{0}\n" }
|
674
|
+
bc = 0
|
675
|
+
@regexp ||= Regexp.new(classes.join + tokens.map{|part|
|
676
|
+
r = part.to_r_source(bc)
|
677
|
+
bc += part.size
|
678
|
+
r
|
679
|
+
}.join, Regexp::EXTENDED)
|
680
|
+
end
|
681
|
+
|
682
|
+
|
683
|
+
# Extracts variables from a uri ( given as string ) or an instance of MatchData ( which was matched by the regexp of this template.
|
684
|
+
# The actual result depends on the value of @p post_processing.
|
685
|
+
# This argument specifies whether pair arrays should be converted to hashes.
|
686
|
+
#
|
687
|
+
# @example
|
688
|
+
# URITemplate::Draft7.new('{var}').extract('value') #=> {'var'=>'value'}
|
689
|
+
# URITemplate::Draft7.new('{&args*}').extract('&a=1&b=2') #=> {'args'=>{'a'=>'1','b'=>'2'}}
|
690
|
+
# URITemplate::Draft7.new('{&arg,arg}').extract('&arg=1&arg=2') #=> {'arg'=>'2'}
|
691
|
+
#
|
692
|
+
# @example
|
693
|
+
# URITemplate::Draft7.new('{var}').extract('value', URITemplate::Draft7::NO_PROCESSING) #=> [['var','value']]
|
694
|
+
# URITemplate::Draft7.new('{&args*}').extract('&a=1&b=2', URITemplate::Draft7::NO_PROCESSING) #=> [['args',[['a','1'],['b','2']]]]
|
695
|
+
# URITemplate::Draft7.new('{&arg,arg}').extract('&arg=1&arg=2', URITemplate::Draft7::NO_PROCESSING) #=> [['arg','1'],['arg','2']]
|
696
|
+
#
|
697
|
+
#
|
698
|
+
def extract(uri_or_match, post_processing = DEFAULT_PROCESSING )
|
699
|
+
if uri_or_match.kind_of? String
|
700
|
+
m = self.to_r.match(uri_or_match)
|
701
|
+
elsif uri_or_match.kind_of?(MatchData)
|
702
|
+
if uri_or_match.regexp != self.to_r
|
703
|
+
raise ArgumentError, "Trying to extract variables from MatchData which was not generated by this template."
|
704
|
+
end
|
705
|
+
m = uri_or_match
|
706
|
+
elsif uri_or_match.nil?
|
707
|
+
return nil
|
708
|
+
else
|
709
|
+
raise ArgumentError, "Expected to receive a String or a MatchData, but got #{uri_or_match.inspect}."
|
710
|
+
end
|
711
|
+
if m.nil?
|
712
|
+
return nil
|
713
|
+
else
|
714
|
+
result = extract_matchdata(m)
|
715
|
+
if post_processing.include? :convert_values
|
716
|
+
result.map!{|k,v| [k, Utils.pair_array_to_hash(v)] }
|
717
|
+
end
|
718
|
+
|
719
|
+
if post_processing.include? :convert_result
|
720
|
+
result = Utils.pair_array_to_hash(result)
|
721
|
+
end
|
722
|
+
|
723
|
+
if block_given?
|
724
|
+
return yield result
|
725
|
+
end
|
726
|
+
|
727
|
+
return result
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
# Extracts variables without any proccessing.
|
732
|
+
# This is equivalent to {#extract} with options {NO_PROCESSING}.
|
733
|
+
# @see #extract
|
734
|
+
def extract_simple(uri_or_match)
|
735
|
+
extract( uri_or_match, NO_PROCESSING )
|
736
|
+
end
|
737
|
+
|
738
|
+
# Returns the pattern for this template.
|
739
|
+
def pattern
|
740
|
+
@pattern ||= tokens.map(&:to_s).join
|
741
|
+
end
|
742
|
+
|
743
|
+
alias to_s pattern
|
744
|
+
|
745
|
+
# Sections are a custom extension to the uri template spec.
|
746
|
+
# A template section ( in comparison to a template ) can be unbounded on its ends. Therefore they don't necessarily match a whole uri and can be concatenated.
|
747
|
+
# Unboundedness is denoted with unicode character \u2026 ( … ).
|
748
|
+
#
|
749
|
+
# @example
|
750
|
+
# prefix = URITemplate::Draft7::Section.new('/prefix…')
|
751
|
+
# template = URITemplate::Draft7.new('/prefix')
|
752
|
+
# prefix === '/prefix/something completly different' #=> true
|
753
|
+
# template === '/prefix/something completly different' #=> false
|
754
|
+
# prefix.to_r.match('/prefix/something completly different').post_match #=> '/something completly different'
|
755
|
+
#
|
756
|
+
# @example
|
757
|
+
# prefix = URITemplate::Draft7::Section.new('/prefix…')
|
758
|
+
# tpl = prefix >> '…/end'
|
759
|
+
# tpl.pattern #=> '/prefix/end'
|
760
|
+
#
|
761
|
+
# This behavior is usefull for building routers:
|
762
|
+
#
|
763
|
+
# @example
|
764
|
+
#
|
765
|
+
# def route( uri )
|
766
|
+
# prefixes = [
|
767
|
+
# [ 'app_a/…' , lambda{|vars, rest| "app_a: #{rest} with #{vars.inspect}" } ],
|
768
|
+
# [ 'app_b/{x}/…', lambda{|vars, rest| "app_b: #{rest} with #{vars.inspect}" } ]
|
769
|
+
# ]
|
770
|
+
# prefixes.each do |tpl, lb|
|
771
|
+
# tpl = URITemplate::Draft7::Section.new(tpl)
|
772
|
+
# tpl.match(uri) do |match_data|
|
773
|
+
# return lb.call(tpl.extract(match_data), match_data.post_match)
|
774
|
+
# end
|
775
|
+
# end
|
776
|
+
# return "not found"
|
777
|
+
# end
|
778
|
+
# route( 'app_a/do_something' ) #=> "app_a: do_something with {}"
|
779
|
+
# route( 'app_b/1337/something_else' ) #=> "app_b: something_else with {\"x\"=>\"1337\"}"
|
780
|
+
# route( 'bla' ) #=> 'not found'
|
781
|
+
#
|
782
|
+
class Section < self
|
783
|
+
|
784
|
+
include URITemplate::Section
|
785
|
+
|
786
|
+
# The ellipsis character.
|
787
|
+
ELLIPSIS = "\u2026".freeze
|
788
|
+
|
789
|
+
# Is this section left bounded?
|
790
|
+
def left_bound?
|
791
|
+
tokens.first.kind_of? LeftBound
|
792
|
+
end
|
793
|
+
|
794
|
+
# Is this section right bounded?
|
795
|
+
def right_bound?
|
796
|
+
tokens.last.kind_of? RightBound
|
797
|
+
end
|
798
|
+
|
799
|
+
# Concatenates this section with anything that can be coerced into a section.
|
800
|
+
#
|
801
|
+
# @example
|
802
|
+
# sect = URITemplate::Draft7::Section.new('/prefix…')
|
803
|
+
# sect >> '…/mid…' >> '…/end' # URITemplate::Draft7::Section.new('/prefix/mid/end')
|
804
|
+
#
|
805
|
+
# @return Section
|
806
|
+
def >>(other)
|
807
|
+
o = self.class.try_convert(other)
|
808
|
+
if o.kind_of? Section
|
809
|
+
if !self.right_bound? and !o.left_bound?
|
810
|
+
return self.class.new(self.tokens[0..-2] + o.tokens[1..-1], o.options)
|
811
|
+
end
|
812
|
+
else
|
813
|
+
raise ArgumentError, "Expected something that could be converted to a URITemplate section, but got #{other.inspect}"
|
814
|
+
end
|
815
|
+
end
|
816
|
+
|
817
|
+
protected
|
818
|
+
# @private
|
819
|
+
def tokenize!
|
820
|
+
pat = pattern
|
821
|
+
if pat == ELLIPSIS
|
822
|
+
return [Open.new]
|
823
|
+
end
|
824
|
+
lb = (pat[0] != ELLIPSIS)
|
825
|
+
rb = (pat[-1] != ELLIPSIS)
|
826
|
+
pat = pat[ (lb ? 0 : 1)..(rb ? -1 : -2) ]
|
827
|
+
[lb ? LeftBound.new : Open.new] + Tokenizer.new(pat).to_a + [rb ? RightBound.new : Open.new]
|
828
|
+
end
|
829
|
+
|
830
|
+
end
|
831
|
+
|
832
|
+
# Compares two template patterns.
|
833
|
+
def ==(tpl)
|
834
|
+
return false if self.class != tpl.class
|
835
|
+
return self.pattern == tpl.pattern
|
836
|
+
end
|
837
|
+
|
838
|
+
# @method ===(uri)
|
839
|
+
# Alias for to_r.=== . Tests whether this template matches a given uri.
|
840
|
+
# @return TrueClass, FalseClass
|
841
|
+
def_delegators :to_r, :===
|
842
|
+
|
843
|
+
# @method match(uri)
|
844
|
+
# Alias for to_r.match . Matches this template against the given uri.
|
845
|
+
# @yield MatchData
|
846
|
+
# @return MatchData, Object
|
847
|
+
def_delegators :to_r, :match
|
848
|
+
|
849
|
+
protected
|
850
|
+
# @private
|
851
|
+
def tokenize!
|
852
|
+
[LeftBound.new] + Tokenizer.new(pattern).to_a + [RightBound.new]
|
853
|
+
end
|
854
|
+
|
855
|
+
def tokens
|
856
|
+
@tokens ||= tokenize!
|
857
|
+
end
|
858
|
+
|
859
|
+
# @private
|
860
|
+
def extract_matchdata(matchdata)
|
861
|
+
bc = 0
|
862
|
+
vars = []
|
863
|
+
tokens.each{|part|
|
864
|
+
i = 0
|
865
|
+
while i < part.size
|
866
|
+
vars.push(*part.extract(i, matchdata["v#{bc}"]))
|
867
|
+
bc += 1
|
868
|
+
i += 1
|
869
|
+
end
|
870
|
+
}
|
871
|
+
return vars
|
872
|
+
end
|
873
|
+
|
874
|
+
end
|
875
|
+
|
876
|
+
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# This program is free software: you can redistribute it and/or modify
|
3
|
+
# it under the terms of the Affero GNU General Public License as published by
|
4
|
+
# the Free Software Foundation, either version 3 of the License, or
|
5
|
+
# (at your option) any later version.
|
6
|
+
#
|
7
|
+
# This program is distributed in the hope that it will be useful,
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
10
|
+
# GNU General Public License for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License
|
13
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
14
|
+
#
|
15
|
+
# (c) 2011 by Hannes Georg
|
16
|
+
#
|
17
|
+
|
18
|
+
module URITemplate
|
19
|
+
|
20
|
+
# This error will be raised whenever an object could not be converted to a param string.
|
21
|
+
class Unconvertable < StandardError
|
22
|
+
|
23
|
+
attr_reader :object
|
24
|
+
|
25
|
+
def initialize(object)
|
26
|
+
@object = object
|
27
|
+
super("Could not convert the given object (#{Object.instance_method(:inspect).bind(@object).call() rescue '<????>'}) to a param since it doesn't respond to :to_param or :to_s.")
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
# A collection of some utility methods
|
33
|
+
module Utils
|
34
|
+
|
35
|
+
# @private
|
36
|
+
PCT = /%(\h\h)/.freeze
|
37
|
+
|
38
|
+
# A regexp which match all non-simple characters.
|
39
|
+
NOT_SIMPLE_CHARS = /([^A-Za-z0-9\-\._])/.freeze
|
40
|
+
|
41
|
+
# Encodes the given string into a pct-encoded string.
|
42
|
+
# @param s The string to be encoded.
|
43
|
+
# @param m A regexp matching all characters, which need to be encoded.
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# URITemplate::Utils.pct("abc") #=> "abc"
|
47
|
+
# URITemplate::Utils.pct("%") #=> "%25"
|
48
|
+
#
|
49
|
+
def pct(s, m=NOT_SIMPLE_CHARS)
|
50
|
+
s.to_s.encode('UTF-8').gsub(m){
|
51
|
+
'%'+$1.unpack('H2'*$1.bytesize).join('%').upcase
|
52
|
+
}.encode('ASCII')
|
53
|
+
end
|
54
|
+
|
55
|
+
# Decodes the given pct-encoded string into a utf-8 string.
|
56
|
+
# Should be the opposite of #pct.
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# URITemplate::Utils.dpct("abc") #=> "abc"
|
60
|
+
# URITemplate::Utils.dpct("%25") #=> "%"
|
61
|
+
#
|
62
|
+
def dpct(s)
|
63
|
+
s.to_s.encode('ASCII').gsub(PCT){
|
64
|
+
$1.to_i(16).chr
|
65
|
+
}.encode('UTF-8')
|
66
|
+
end
|
67
|
+
|
68
|
+
# Converts an object to a param value.
|
69
|
+
# Tries to call :to_param and then :to_s on that object.
|
70
|
+
# @raise Unconvertable if the object could not be converted.
|
71
|
+
# @example
|
72
|
+
# URITemplate::Utils.object_to_param(5) #=> "5"
|
73
|
+
# o = Object.new
|
74
|
+
# def o.to_param
|
75
|
+
# "42"
|
76
|
+
# end
|
77
|
+
# URITemplate::Utils.object_to_param(o) #=> "42"
|
78
|
+
def object_to_param(object)
|
79
|
+
if object.respond_to? :to_param
|
80
|
+
object.to_param
|
81
|
+
elsif object.respond_to? :to_s
|
82
|
+
object.to_s
|
83
|
+
else
|
84
|
+
raise Unconvertable.new(object)
|
85
|
+
end
|
86
|
+
rescue NoMethodError
|
87
|
+
raise Unconvertable.new(object)
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
# Returns true when the given value is an array and it only consists of arrays with two items.
|
92
|
+
# This useful when using a hash is not ideal, since it doesn't allow duplicate keys.
|
93
|
+
# @example
|
94
|
+
# URITemplate::Utils.pair_array?( Object.new ) #=> false
|
95
|
+
# URITemplate::Utils.pair_array?( [] ) #=> true
|
96
|
+
# URITemplate::Utils.pair_array?( [1,2,3] ) #=> false
|
97
|
+
# URITemplate::Utils.pair_array?( [ ['a',1],['b',2],['c',3] ] ) #=> true
|
98
|
+
# URITemplate::Utils.pair_array?( [ ['a',1],['b',2],['c',3],[] ] ) #=> false
|
99
|
+
def pair_array?(a)
|
100
|
+
return false unless a.kind_of? Array
|
101
|
+
return a.all?{|p| p.kind_of? Array and p.size == 2 }
|
102
|
+
end
|
103
|
+
|
104
|
+
# Turns the given value into a hash if it is an array of pairs.
|
105
|
+
# Otherwise it returns the value.
|
106
|
+
# You can test whether a value will be converted with {#pair_array?}.
|
107
|
+
# @example
|
108
|
+
# URITemplate::Utils.pair_array_to_hash( 'x' ) #=> 'x'
|
109
|
+
# URITemplate::Utils.pair_array_to_hash( [ ['a',1],['b',2],['c',3] ] ) #=> {'a'=>1,'b'=>2,'c'=>3}
|
110
|
+
# URITemplate::Utils.pair_array_to_hash( [ ['a',1],['a',2],['a',3] ] ) #=> {'a'=>3}
|
111
|
+
def pair_array_to_hash(a)
|
112
|
+
if pair_array?(a)
|
113
|
+
return Hash[ *a.map{ |k,v| [k,pair_array_to_hash(v)] }.flatten(1) ]
|
114
|
+
else
|
115
|
+
return a
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
|
121
|
+
extend self
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'uri_template'
|
3
|
+
s.version = '0.0.1'
|
4
|
+
s.date = '2011-10-30'
|
5
|
+
s.authors = ["HannesG"]
|
6
|
+
s.email = %q{hannes.georg@googlemail.com}
|
7
|
+
s.summary = 'A templating system for URIs.'
|
8
|
+
s.homepage = 'http://github.com/hannesg/uri_template'
|
9
|
+
s.description = 'A templating system for URIs, which implements http://tools.ietf.org/html/draft-gregorio-uritemplate-07 . An implementation of an older version of that spec is known as addressable. This system however is intended to be extended when newer specs evolve. For now only draft 7 is supported. Downside: only for 1.9 compatible since it uses Oniguruma regexp.'
|
10
|
+
|
11
|
+
s.require_paths = ['lib']
|
12
|
+
|
13
|
+
s.files = Dir.glob('lib/**/**/*.rb') + ['uri_template.gemspec', 'README']
|
14
|
+
|
15
|
+
s.add_development_dependency 'rspec'
|
16
|
+
s.add_development_dependency 'yard'
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: uri_template
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- HannesG
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-30 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &14233820 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *14233820
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: yard
|
27
|
+
requirement: &14636440 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *14636440
|
36
|
+
description: ! 'A templating system for URIs, which implements http://tools.ietf.org/html/draft-gregorio-uritemplate-07
|
37
|
+
. An implementation of an older version of that spec is known as addressable. This
|
38
|
+
system however is intended to be extended when newer specs evolve. For now only
|
39
|
+
draft 7 is supported. Downside: only for 1.9 compatible since it uses Oniguruma
|
40
|
+
regexp.'
|
41
|
+
email: hannes.georg@googlemail.com
|
42
|
+
executables: []
|
43
|
+
extensions: []
|
44
|
+
extra_rdoc_files: []
|
45
|
+
files:
|
46
|
+
- lib/uri_template.rb
|
47
|
+
- lib/uri_template/draft7.rb
|
48
|
+
- lib/uri_template/utils.rb
|
49
|
+
- uri_template.gemspec
|
50
|
+
- README
|
51
|
+
homepage: http://github.com/hannesg/uri_template
|
52
|
+
licenses: []
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
requirements: []
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.8.10
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: A templating system for URIs.
|
75
|
+
test_files: []
|
76
|
+
has_rdoc:
|