uri_template 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+
@@ -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: