uri_template 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
1
+ # 0.3.0 - 24.05.2012
2
+ * Implemented the final version. Default implementation is now RFC 6570
3
+ * BUGFIX: variables with terminal dots were allowed
4
+ * BUGFIX: lists of commas were parsed incorrectly
5
+
1
6
  # 0.2.1 - 30.12.2011
2
7
  * Compatibility: Works now with MRI 1.8.7 and REE
3
8
 
data/README CHANGED
@@ -1,10 +1,10 @@
1
1
  = URITemplate - a uri template library
2
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.
3
+ With URITemplate you can generate URI based on simple templates. The syntax if defined by RFC 6570 ( http://tools.ietf.org/html/rfc6570 ).
4
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.
5
+ For backward compatibility, there is an implementation based on draft 7 of the uri template spec ( http://tools.ietf.org/html/draft-gregorio-uritemplate-07 ), too. For the syntax read that spec. Draft 2 of that specification is implemented as addressable gem.
6
6
 
7
- Some implementations might be able to extract variables, too ( Draft 7 is! ).
7
+ 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.
8
8
 
9
9
  From version 0.2.0, it will use escape_utils if available. This will significantly boost uri-escape/unescape performance if more characters need to be escaped ( may be slightly slower in trivial cases. working on that ... ), but does not run everywhere. To enable this, do the following:
10
10
 
@@ -34,30 +34,32 @@ From version 0.2.0, it will use escape_utils if available. This will significant
34
34
 
35
35
  == Benchmarks
36
36
 
37
- * System: Core 2 Duo T9300, 4 gb ram, Fedora 16 64 bit, ruby 1.9.3, 100_000 repetitions
38
- * Implementation: Draft7 ( version 0.1.3 (pre) ) vs. Addressable ( 2.2.6 )
37
+ * System: Core 2 Duo T9300, 4 gb ram, ubuntu 12.04 64 bit, ruby 1.9.3, 100_000 repetitions
38
+ * Implementation: RFC6570 ( version 0.3.0 ) vs. Addressable ( 2.2.8 )
39
39
  * Results marked with * means that the template object is reused.
40
40
 
41
- Addressable | Addressable* | Draft7 | Draft7* |
42
- --Expansion----------------------------------------------------------------------------------------
43
- Empty string 8.601 | 8.529 | 0.451 | 0.057 |
44
- One simple variable 19.832 | 19.675 | 2.746 | 0.928 |
45
- One escaped variable 22.437 | 22.151 | 6.011 | 4.242 |
46
- One missing variable 9.638 | 9.544 | 1.855 | 0.184 |
47
- Path segments 25.353 | 25.413 | 3.171 | 1.372 |
48
- Arguments 27.140 | 27.144 | 5.293 | 2.715 |
49
- Full URI 75.599 | 75.680 | 11.147 | 5.246 |
50
- Segments and Arguments 73.360 | 73.334 | 7.694 | 3.604 |
51
- --Extraction---------------------------------------------------------------------------------------
52
- Empty string 14.031 | 13.993 | 1.849 | 0.576 |
53
- One simple variable 28.701 | 28.503 | 6.988 | 1.840 |
54
- One escaped variable 33.237 | 33.358 | 8.354 | 3.155 |
55
- One missing variable 17.201 | 17.051 | 6.318 | 1.177 |
56
- Path segments 43.112 | 43.623 | 12.502 | 2.983 |
57
- Arguments 54.448 | 54.090 | 13.772 | 3.394 |
58
- Full URI 95.433 | 91.961 | 30.762 | 6.797 |
59
- Segments and Arguments 80.508 | 79.570 | 23.204 | 5.210 |
60
- Segments and Arguments ( not extractable ) 22.079 | 21.967 | 18.017 | 0.256 |
61
-
62
- SUCCESS - Draft7 was faster in every test!
41
+ Addressable | Addressable* | RFC6570 | RFC6570* |
42
+ --Expansion-----------------------------------------------------------------------------------------
43
+ Empty string 8.505 | 8.439 | 0.539 | 0.064 |
44
+ One simple variable 19.717 | 19.721 | 3.031 | 1.169 |
45
+ One escaped variable 21.873 | 22.017 | 3.573 | 1.705 |
46
+ One missing variable 9.676 | 11.981 | 2.633 | 0.352 |
47
+ Path segments 29.901 | 31.698 | 4.929 | 3.051 |
48
+ Arguments 36.584 | 36.531 | 9.540 | 5.982 |
49
+ Full URI 109.102 | 116.458 | 19.806 | 10.548 |
50
+ Segments and Arguments 108.103 | 107.750 | 14.059 | 7.593 |
51
+ total 343.461 | 354.596 | 58.109 | 30.464 |
52
+ --Extraction----------------------------------------------------------------------------------------
53
+ Empty string 21.422 | 21.843 | 3.122 | 0.804 |
54
+ One simple variable 39.860 | 43.840 | 10.671 | 2.874 |
55
+ One escaped variable 51.321 | 50.790 | 10.963 | 2.040 |
56
+ One missing variable 26.125 | 26.320 | 9.400 | 1.847 |
57
+ Path segments 62.712 | 64.191 | 18.502 | 4.518 |
58
+ Arguments 81.350 | 81.258 | 20.762 | 5.401 |
59
+ Full URI 145.339 | 141.795 | 45.306 | 11.096 |
60
+ Segments and Arguments 124.431 | 122.885 | 34.208 | 8.126 |
61
+ Segments and Arguments ( not extractable ) 34.183 | 34.200 | 26.542 | 0.410 |
62
+ total 586.743 | 587.122 | 179.476 | 37.116 |
63
+
64
+ SUCCESS - URITemplate was faster in every test!
63
65
 
@@ -12,7 +12,7 @@
12
12
  # You should have received a copy of the GNU General Public License
13
13
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
14
  #
15
- # (c) 2011 by Hannes Georg
15
+ # (c) 2011 - 2012 by Hannes Georg
16
16
  #
17
17
 
18
18
  # A base module for all implementations of a uri template.
@@ -21,8 +21,13 @@ module URITemplate
21
21
  # @private
22
22
  # Should we use \u.... or \x.. in regexps?
23
23
  SUPPORTS_UNICODE_CHARS = begin
24
- rx = eval('/\u0020/')
25
- !!(rx =~ " ")
24
+ if "string".respond_to? :encoding
25
+ rx = eval('Regexp.compile("\u0020")')
26
+ !!(rx =~ " ")
27
+ else
28
+ rx = eval('/\u0020/')
29
+ !!(rx =~ " ")
30
+ end
26
31
  rescue SyntaxError
27
32
  false
28
33
  end
@@ -84,6 +89,7 @@ module URITemplate
84
89
 
85
90
  autoload :Utils, 'uri_template/utils'
86
91
  autoload :Draft7, 'uri_template/draft7'
92
+ autoload :RFC6570, 'uri_template/rfc6570'
87
93
  autoload :Colon, 'uri_template/colon'
88
94
 
89
95
  # A hash with all available implementations.
@@ -91,9 +97,10 @@ module URITemplate
91
97
  # @see resolve_class
92
98
  VERSIONS = {
93
99
  :draft7 => :Draft7,
100
+ :rfc6570 => :RFC6570,
94
101
  :default => :Draft7,
95
102
  :colon => :Colon,
96
- :latest => :Draft7
103
+ :latest => :RFC6570
97
104
  }
98
105
 
99
106
  # Looks up which implementation to use.
@@ -207,12 +214,18 @@ module URITemplate
207
214
  module Invalid
208
215
  end
209
216
 
217
+ # A base class for all errors which will be raised when a variable value
218
+ # is not allowed for a certain expansion.
219
+ module InvalidValue
220
+ end
221
+
210
222
  # Expands this uri template with the given variables.
211
223
  # The variables should be converted to strings using {Utils#object_to_param}.
212
224
  # @raise {Unconvertable} if a variable could not be converted to a string.
213
225
  # @param variables Hash
214
226
  # @return String
215
227
  def expand(variables = {})
228
+ raise ArgumentError, "Expected something that returns to :[], but got: #{variables.inspect}" unless variables.respond_to? :[]
216
229
  tokens.map{|part|
217
230
  part.expand(variables)
218
231
  }.join
@@ -12,7 +12,7 @@
12
12
  # You should have received a copy of the GNU General Public License
13
13
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
14
  #
15
- # (c) 2011 by Hannes Georg
15
+ # (c) 2011 - 2012 by Hannes Georg
16
16
  #
17
17
 
18
18
  require 'forwardable'
@@ -97,7 +97,7 @@ class Colon
97
97
 
98
98
  # Extracts variables from an uri.
99
99
  #
100
- # @param String uri
100
+ # @param uri [String]
101
101
  # @return nil,Hash
102
102
  def extract(uri)
103
103
  md = self.to_r.match(uri)
@@ -12,190 +12,66 @@
12
12
  # You should have received a copy of the GNU General Public License
13
13
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
14
  #
15
- # (c) 2011 by Hannes Georg
15
+ # (c) 2011 - 2012 by Hannes Georg
16
16
  #
17
17
 
18
- require 'strscan'
19
- require 'set'
20
- require 'forwardable'
21
-
22
- require 'uri_template'
23
- require 'uri_template/utils'
24
18
 
25
19
  # 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
- if SUPPORTS_UNICODE_CHARS
37
- # @private
38
- # \/ - unicode ctrl-chars
39
- LITERAL = /([^"'%<>\\^`{|}\u0000-\u001F\u007F-\u009F\s]|%[0-9a-fA-F]{2})+/u
40
- else
41
- # @private
42
- LITERAL = Regexp.compile('([^"\'%<>\\\\^`{|}\x00-\x1F\x7F-\x9F\s]|%[0-9a-fA-F]{2})+',Utils::KCODE_UTF8)
43
- end
44
-
45
- # @private
46
- CHARACTER_CLASSES = {
47
-
48
- :unreserved => {
49
- :class => '(?:[A-Za-z0-9\-\._]|%[0-9a-fA-F]{2})',
50
- :grabs_comma => false
51
- },
52
- :unreserved_reserved_pct => {
53
- :class => '(?:[A-Za-z0-9\-\._:\/?#\[\]@!\$%\'\(\)*+,;=]|%[0-9a-fA-F]{2})',
54
- :grabs_comma => true
55
- },
56
-
57
- :varname => {
58
- :class => '(?:[a-zA-Z_]|%[0-9a-fA-F]{2})(?:[a-zA-Z_\.]|%[0-9a-fA-F]{2})*?',
59
- :class_name => 'c_vn_'
60
- }
61
-
62
- }
63
-
64
- # Specifies that no processing should be done upon extraction.
65
- # @see #extract
66
- NO_PROCESSING = []
67
-
68
- # Specifies that the extracted values should be processed.
69
- # @see #extract
70
- CONVERT_VALUES = [:convert_values]
71
-
72
- # Specifies that the extracted variable list should be processed.
73
- # @see #extract
74
- CONVERT_RESULT = [:convert_result]
75
-
76
- # Default processing. Means: convert values and the list itself.
77
- # @see #extract
78
- DEFAULT_PROCESSING = CONVERT_VALUES + CONVERT_RESULT
79
-
80
- # @private
81
- VAR = Regexp.compile(<<'__REGEXP__'.strip, Utils::KCODE_UTF8)
82
- ((?:[a-zA-Z_]|%[0-9a-fA-F]{2})(?:[a-zA-Z_\.]|%[0-9a-fA-F]{2})*)(\*)?(?::(\d+))?
83
- __REGEXP__
84
-
85
- # @private
86
- EXPRESSION = Regexp.compile(<<'__REGEXP__'.strip, Utils::KCODE_UTF8)
87
- \{([+#\./;?&]?)((?:[a-zA-Z_]|%[0-9a-fA-F]{2})(?:[a-zA-Z_\.]|%[0-9a-fA-F]{2})*\*?(?::\d+)?(?:,(?:[a-zA-Z_]|%[0-9a-fA-F]{2})(?:[a-zA-Z_\.]|%[0-9a-fA-F]{2})*\*?(?::\d+)?)*)\}
88
- __REGEXP__
89
-
90
- # @private
91
- URI = Regexp.compile(<<__REGEXP__.strip, Utils::KCODE_UTF8)
92
- \\A(#{LITERAL.source}|#{EXPRESSION.source})*\\z
93
- __REGEXP__
94
-
95
- SLASH = ?/
96
-
97
- # @private
98
- class Token
99
- end
100
-
101
- # @private
102
- class Literal < Token
20
+ # This class is here for backward compatibility. There is already a newer draft of the spec and an rfc.
21
+ class URITemplate::Draft7 < URITemplate::RFC6570
103
22
 
104
- include URITemplate::Literal
23
+ TYPE = :draft7
105
24
 
106
- def initialize(string)
107
- @string = string
108
- end
109
-
110
- def level
111
- 1
112
- end
113
-
114
- def arity
115
- 0
116
- end
117
-
118
- def to_r_source(*_)
119
- Regexp.escape(@string)
120
- end
121
-
122
- def to_s
123
- @string
124
- end
25
+ CHARACTER_CLASSES = URITemplate::RFC6570::CHARACTER_CLASSES
125
26
 
126
- end
127
-
128
- # @private
129
- class Expression < Token
130
-
131
- include URITemplate::Expression
132
-
133
- attr_reader :variables, :max_length
134
-
135
- def initialize(vars)
136
- @variable_specs = vars
137
- @variables = vars.map(&:first)
138
- @variables.uniq!
139
- end
140
-
141
- PREFIX = ''.freeze
142
- SEPARATOR = ','.freeze
143
- PAIR_CONNECTOR = '='.freeze
144
- PAIR_IF_EMPTY = true
145
- LIST_CONNECTOR = ','.freeze
146
- BASE_LEVEL = 1
147
-
148
- CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved]
27
+ Utils = URITemplate::Utils
149
28
 
150
- NAMED = false
151
- OPERATOR = ''
29
+ class Expression < URITemplate::RFC6570::Expression
152
30
 
153
- def level
154
- if @variable_specs.none?{|_,expand,ml| expand || (ml > 0) }
155
- if @variable_specs.size == 1
156
- return self.class::BASE_LEVEL
157
- else
158
- return 3
159
- end
160
- else
161
- return 4
31
+ def extract(position,matched)
32
+ name, expand, max_length = @variable_specs[position]
33
+ if matched.nil?
34
+ return [[ name , matched ]]
162
35
  end
163
- end
164
-
165
- def arity
166
- @variable_specs.size
167
- end
168
-
169
- def expand( vars )
170
- result = []
171
- @variable_specs.each{| var, expand , max_length |
172
- unless vars[var].nil?
173
- if vars[var].kind_of?(Hash) or Utils.pair_array?(vars[var])
174
- result.push( *transform_hash(var, vars[var], expand, max_length) )
175
- elsif vars[var].kind_of? Array
176
- result.push( *transform_array(var, vars[var], expand, max_length) )
36
+ if expand
37
+ #TODO: do we really need this? - this could be stolen from rack
38
+ ex = self.class.hash_extractor(max_length)
39
+ rest = matched
40
+ splitted = []
41
+ found_value = false
42
+ # 1 = name
43
+ # 2 = value
44
+ # 3 = rest
45
+ until rest.size == 0
46
+ match = ex.match(rest)
47
+ if match.nil?
48
+ raise "Couldn't match #{rest.inspect} againts the hash extractor. This is definitly a Bug. Please report this ASAP!"
49
+ end
50
+ if match.post_match.size == 0
51
+ rest = match[3].to_s
177
52
  else
178
- if self.class::NAMED
179
- result.push( pair(var, vars[var], max_length) )
180
- else
181
- result.push( cut( escape(vars[var]), max_length ) )
182
- end
53
+ rest = ''
54
+ end
55
+ if match[1]
56
+ found_value = true
57
+ splitted << [ match[1][0..-2], decode(match[2] + rest , false) ]
58
+ else
59
+ splitted << [ match[2] + rest, nil ]
183
60
  end
61
+ rest = match.post_match
184
62
  end
185
- }
186
- if result.any?
187
- return (self.class::PREFIX + result.join(self.class::SEPARATOR))
188
- else
189
- return ''
63
+ if !found_value
64
+ return [ [ name, splitted.map{|n,v| decode(n , false) } ] ]
65
+ else
66
+ return [ [ name, splitted ] ]
67
+ end
68
+ elsif self.class::NAMED
69
+ return [ [ name, decode( matched[1..-1] ) ] ]
190
70
  end
191
- end
192
71
 
193
- def to_s
194
- return '{' + self.class::OPERATOR + @variable_specs.map{|name,expand,max_length| name + (expand ? '*': '') + (max_length > 0 ? (':' + max_length.to_s) : '') }.join(',') + '}'
72
+ return [ [ name, decode( matched ) ] ]
195
73
  end
196
74
 
197
- #TODO: certain things after a slurpy variable will never get matched. therefore, it's pointless to add expressions for them
198
- #TODO: variables, which appear twice could be compacted, don't they?
199
75
  def to_r_source
200
76
  source = []
201
77
  first = true
@@ -262,52 +138,18 @@ __REGEXP__
262
138
  return '(?:' + Regexp.escape(self.class::PREFIX) + source.join + ')?'
263
139
  end
264
140
 
265
- def extract(position,matched)
266
- name, expand, max_length = @variable_specs[position]
267
- if matched.nil?
268
- return [[ name , matched ]]
269
- end
141
+ protected
142
+
143
+ def transform_array(name, ary, expand , max_length)
270
144
  if expand
271
- #TODO: do we really need this? - this could be stolen from rack
272
- ex = self.class.hash_extractor(max_length)
273
- rest = matched
274
- splitted = []
275
- found_value = false
276
- # 1 = name
277
- # 2 = value
278
- # 3 = rest
279
- until rest.size == 0
280
- match = ex.match(rest)
281
- if match.nil?
282
- raise "Couldn't match #{rest.inspect} againts the hash extractor. This is definitly a Bug. Please report this ASAP!"
283
- end
284
- if match.post_match.size == 0
285
- rest = match[3].to_s
286
- else
287
- rest = ''
288
- end
289
- if match[1]
290
- found_value = true
291
- splitted << [ match[1][0..-2], decode(match[2] + rest , false) ]
292
- else
293
- splitted << [ match[2] + rest, nil ]
294
- end
295
- rest = match.post_match
296
- end
297
- if !found_value
298
- return [ [ name, splitted.map{|n,v| decode(n , false) } ] ]
299
- else
300
- return [ [ name, splitted ] ]
301
- end
302
- elsif self.class::NAMED
303
- return [ [ name, decode( matched[1..-1] ) ] ]
145
+ ary.map{|value| escape(value) }
146
+ elsif ary.none?
147
+ []
148
+ else
149
+ [ (self.class::NAMED ? escape(name)+self.class::PAIR_CONNECTOR : '' ) + ary.map{|value| escape(value) }.join(self.class::LIST_CONNECTOR) ]
304
150
  end
305
-
306
- return [ [ name, decode( matched ) ] ]
307
151
  end
308
152
 
309
- protected
310
-
311
153
  module ClassMethods
312
154
 
313
155
  def hash_extractor(max_length)
@@ -324,88 +166,9 @@ __REGEXP__
324
166
 
325
167
  extend ClassMethods
326
168
 
327
- def escape(x)
328
- Utils.escape_url(Utils.object_to_param(x))
329
- end
330
-
331
- def unescape(x)
332
- Utils.unescape_url(x)
333
- end
334
-
335
- SPLITTER = /^(?:,(,*)|([^,]+))/
336
-
337
- def decode(x, split = true)
338
- if x.nil?
339
- if self.class::PAIR_IF_EMPTY
340
- return x
341
- else
342
- return ''
343
- end
344
- elsif split
345
- r = []
346
- v = x
347
- until v.size == 0
348
- m = SPLITTER.match(v)
349
- if m[1] and m[1].size > 0
350
- r << m[1]
351
- elsif m[2]
352
- r << unescape(m[2])
353
- end
354
- v = m.post_match
355
- end
356
- case(r.size)
357
- when 0 then ''
358
- when 1 then r.first
359
- else r
360
- end
361
- else
362
- unescape(x)
363
- end
364
- end
365
-
366
- def cut(str,chars)
367
- if chars > 0
368
- md = Regexp.compile("\\A#{self.class::CHARACTER_CLASS[:class]}{0,#{chars.to_s}}", Utils::KCODE_UTF8).match(str)
369
- #TODO: handle invalid matches
370
- return md[0]
371
- else
372
- return str
373
- end
374
- end
375
-
376
- def pair(key, value, max_length = 0)
377
- ek = escape(key)
378
- ev = escape(value)
379
- if !self.class::PAIR_IF_EMPTY and ev.size == 0
380
- return ek
381
- else
382
- return ek + self.class::PAIR_CONNECTOR + cut( ev, max_length )
383
- end
384
- end
385
-
386
- def transform_hash(name, hsh, expand , max_length)
387
- if expand
388
- hsh.map{|key,value| pair(key,value) }
389
- elsif hsh.none?
390
- []
391
- else
392
- [ (self.class::NAMED ? escape(name)+self.class::PAIR_CONNECTOR : '' ) + hsh.map{|key,value| escape(key)+self.class::LIST_CONNECTOR+escape(value) }.join(self.class::LIST_CONNECTOR) ]
393
- end
394
- end
395
-
396
- def transform_array(name, ary, expand , max_length)
397
- if expand
398
- ary.map{|value| escape(value) }
399
- elsif ary.none?
400
- []
401
- else
402
- [ (self.class::NAMED ? escape(name)+self.class::PAIR_CONNECTOR : '' ) + ary.map{|value| escape(value) }.join(self.class::LIST_CONNECTOR) ]
403
- end
404
- end
405
-
406
169
  class Reserved < self
407
170
 
408
- CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved_reserved_pct]
171
+ CHARACTER_CLASS = URITemplate::RFC6570::CHARACTER_CLASSES[:unreserved_reserved_pct]
409
172
  OPERATOR = '+'.freeze
410
173
  BASE_LEVEL = 2
411
174
 
@@ -421,7 +184,7 @@ __REGEXP__
421
184
 
422
185
  class Fragment < self
423
186
 
424
- CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved_reserved_pct]
187
+ CHARACTER_CLASS = URITemplate::RFC6570::CHARACTER_CLASSES[:unreserved_reserved_pct]
425
188
  PREFIX = '#'.freeze
426
189
  OPERATOR = '#'.freeze
427
190
  BASE_LEVEL = 2
@@ -483,7 +246,8 @@ __REGEXP__
483
246
  OPERATOR = '&'.freeze
484
247
  BASE_LEVEL = 3
485
248
 
486
- end
249
+ end
250
+
487
251
 
488
252
  end
489
253
 
@@ -499,412 +263,4 @@ __REGEXP__
499
263
  '&' => Expression::FormQueryContinuation
500
264
  }
501
265
 
502
- # This error is raised when an invalid pattern was given.
503
- class Invalid < StandardError
504
-
505
- include URITemplate::Invalid
506
-
507
- attr_reader :pattern, :position
508
-
509
- def initialize(source, position)
510
- @pattern = pattern
511
- @position = position
512
- super("Invalid expression found in #{source.inspect} at #{position}: '#{source[position..-1]}'")
513
- end
514
-
515
- end
516
-
517
- # @private
518
- class Tokenizer
519
-
520
- include Enumerable
521
-
522
- attr_reader :source
523
-
524
- def initialize(source)
525
- @source = source
526
- end
527
-
528
- def each
529
- if !block_given?
530
- return Enumerator.new(self)
531
- end
532
- scanner = StringScanner.new(@source)
533
- until scanner.eos?
534
- expression = scanner.scan(EXPRESSION)
535
- if expression
536
- vars = scanner[2].split(',').map{|name|
537
- match = VAR.match(name)
538
- # 1 = varname
539
- # 2 = explode
540
- # 3 = length
541
- [ match[1], match[2] == '*', match[3].to_i ]
542
- }
543
- yield OPERATORS[scanner[1]].new(vars)
544
- else
545
- literal = scanner.scan(LITERAL)
546
- if literal
547
- yield(Literal.new(literal))
548
- else
549
- raise Invalid.new(@source,scanner.pos)
550
- end
551
- end
552
- end
553
- end
554
-
555
- end
556
-
557
- # The class methods for all draft7 templates.
558
- module ClassMethods
559
-
560
- # Tries to convert the given param in to a instance of {Draft7}
561
- # It basically passes thru instances of that class, parses strings and return nil on everything else.
562
- #
563
- # @example
564
- # URITemplate::Draft7.try_convert( Object.new ) #=> nil
565
- # tpl = URITemplate::Draft7.new('{foo}')
566
- # URITemplate::Draft7.try_convert( tpl ) #=> tpl
567
- # URITemplate::Draft7.try_convert('{foo}') #=> tpl
568
- # URITemplate::Draft7.try_convert(URITemplate.new(:colon, ':foo')) #=> tpl
569
- # # This pattern is invalid, so it wont be parsed:
570
- # URITemplate::Draft7.try_convert('{foo') #=> nil
571
- #
572
- def try_convert(x)
573
- if x.kind_of? self
574
- return x
575
- elsif x.kind_of? String and valid? x
576
- return new(x)
577
- elsif x.kind_of? URITemplate::Colon
578
- return new( x.tokens.map{|tk|
579
- if tk.literal?
580
- Literal.new(tk.string)
581
- else
582
- Expression.new([[tk.variables.first, false, 0]])
583
- end
584
- })
585
- else
586
- return nil
587
- end
588
- end
589
-
590
- # Like {.try_convert}, but raises an ArgumentError, when the conversion failed.
591
- #
592
- # @raise ArgumentError
593
- def convert(x)
594
- o = self.try_convert(x)
595
- if o.nil?
596
- raise ArgumentError, "Expected to receive something that can be converted to an #{self.class}, but got: #{x.inspect}."
597
- else
598
- return o
599
- end
600
- end
601
-
602
- # Tests whether a given pattern is a valid template pattern.
603
- # @example
604
- # URITemplate::Draft7.valid? 'foo' #=> true
605
- # URITemplate::Draft7.valid? '{foo}' #=> true
606
- # URITemplate::Draft7.valid? '{foo' #=> false
607
- def valid?(pattern)
608
- URI === pattern
609
- end
610
-
611
- end
612
-
613
- extend ClassMethods
614
-
615
- attr_reader :options
616
-
617
- # @param String,Array either a pattern as String or an Array of tokens
618
- # @param Hash some options
619
- # @option :lazy If true the pattern will be parsed on first access, this also means that syntax errors will not be detected unless accessed.
620
- def initialize(pattern_or_tokens,options={})
621
- @options = options.dup.freeze
622
- if pattern_or_tokens.kind_of? String
623
- @pattern = pattern_or_tokens.dup
624
- @pattern.freeze
625
- unless @options[:lazy]
626
- self.tokens
627
- end
628
- elsif pattern_or_tokens.kind_of? Array
629
- @tokens = pattern_or_tokens.dup
630
- @tokens.freeze
631
- else
632
- raise ArgumentError, "Expected to receive a pattern string, but got #{pattern_or_tokens.inspect}"
633
- end
634
- end
635
-
636
- # @method expand(variables = {})
637
- # Expands the template with the given variables.
638
- # The expansion should be compatible to uritemplate spec draft 7 ( http://tools.ietf.org/html/draft-gregorio-uritemplate-07 ).
639
- # @note
640
- # All keys of the supplied hash should be strings as anything else won't be recognised.
641
- # @note
642
- # 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.
643
- # @example
644
- # URITemplate::Draft7.new('{foo}').expand('foo'=>'bar') #=> 'bar'
645
- # URITemplate::Draft7.new('{?args*}').expand('args'=>{'key'=>'value'}) #=> '?key=value'
646
- # URITemplate::Draft7.new('{undef}').expand() #=> ''
647
- #
648
- # @param variables Hash
649
- # @return String
650
-
651
- # 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 {#===}.
652
- #
653
- # @example
654
- # tpl = URITemplate::Draft7.new('/foo/{bar}/')
655
- # regex = tpl.to_r
656
- # regex === '/foo/baz/' #=> true
657
- # regex === '/foz/baz/' #=> false
658
- #
659
- # @return Regexp
660
- def to_r
661
- @regexp ||= begin
662
- source = tokens.map(&:to_r_source)
663
- source.unshift('\A')
664
- source.push('\z')
665
- Regexp.new( source.join, Utils::KCODE_UTF8)
666
- end
667
- end
668
-
669
- # Extracts variables from a uri ( given as string ) or an instance of MatchData ( which was matched by the regexp of this template.
670
- # The actual result depends on the value of post_processing.
671
- # This argument specifies whether pair arrays should be converted to hashes.
672
- #
673
- # @example Default Processing
674
- # URITemplate::Draft7.new('{var}').extract('value') #=> {'var'=>'value'}
675
- # URITemplate::Draft7.new('{&args*}').extract('&a=1&b=2') #=> {'args'=>{'a'=>'1','b'=>'2'}}
676
- # URITemplate::Draft7.new('{&arg,arg}').extract('&arg=1&arg=2') #=> {'arg'=>'2'}
677
- #
678
- # @example No Processing
679
- # URITemplate::Draft7.new('{var}').extract('value', URITemplate::Draft7::NO_PROCESSING) #=> [['var','value']]
680
- # URITemplate::Draft7.new('{&args*}').extract('&a=1&b=2', URITemplate::Draft7::NO_PROCESSING) #=> [['args',[['a','1'],['b','2']]]]
681
- # URITemplate::Draft7.new('{&arg,arg}').extract('&arg=1&arg=2', URITemplate::Draft7::NO_PROCESSING) #=> [['arg','1'],['arg','2']]
682
- #
683
- # @raise Encoding::InvalidByteSequenceError when the given uri was not properly encoded.
684
- # @raise Encoding::UndefinedConversionError when the given uri could not be converted to utf-8.
685
- # @raise Encoding::CompatibilityError when the given uri could not be converted to utf-8.
686
- #
687
- # @param [String,MatchData] Uri_or_MatchData A uri or a matchdata from which the variables should be extracted.
688
- # @param [Array] Processing Specifies which processing should be done.
689
- #
690
- # @note
691
- # Don't expect that an extraction can fully recover the expanded variables. Extract rather generates a variable list which should expand to the uri from which it were extracted. In general the following equation should hold true:
692
- # a_tpl.expand( a_tpl.extract( an_uri ) ) == an_uri
693
- #
694
- # @example Extraction cruces
695
- # two_lists = URITemplate::Draft7.new('{listA*,listB*}')
696
- # uri = two_lists.expand('listA'=>[1,2],'listB'=>[3,4]) #=> "1,2,3,4"
697
- # variables = two_lists.extract( uri ) #=> {'listA'=>["1","2","3","4"],'listB'=>nil}
698
- # # However, like said in the note:
699
- # two_lists.expand( variables ) == uri #=> true
700
- #
701
- # @note
702
- # The current implementation drops duplicated variables instead of checking them.
703
- #
704
- #
705
- def extract(uri_or_match, post_processing = DEFAULT_PROCESSING )
706
- if uri_or_match.kind_of? String
707
- m = self.to_r.match(uri_or_match)
708
- elsif uri_or_match.kind_of?(MatchData)
709
- if uri_or_match.respond_to?(:regexp) and uri_or_match.regexp != self.to_r
710
- raise ArgumentError, "Trying to extract variables from MatchData which was not generated by this template."
711
- end
712
- m = uri_or_match
713
- elsif uri_or_match.nil?
714
- return nil
715
- else
716
- raise ArgumentError, "Expected to receive a String or a MatchData, but got #{uri_or_match.inspect}."
717
- end
718
- if m.nil?
719
- return nil
720
- else
721
- result = extract_matchdata(m, post_processing)
722
- if block_given?
723
- return yield result
724
- end
725
-
726
- return result
727
- end
728
- end
729
-
730
- # Extracts variables without any proccessing.
731
- # This is equivalent to {#extract} with options {NO_PROCESSING}.
732
- # @see #extract
733
- def extract_simple(uri_or_match)
734
- extract( uri_or_match, NO_PROCESSING )
735
- end
736
-
737
- # Returns the pattern for this template.
738
- def pattern
739
- @pattern ||= tokens.map(&:to_s).join
740
- end
741
-
742
- alias to_s pattern
743
-
744
- # Compares two template patterns.
745
- def ==(o)
746
- this, other, this_converted, _ = URITemplate.coerce( self, o )
747
- if this_converted
748
- return this == other
749
- end
750
- return this.pattern == other.pattern
751
- end
752
-
753
- # @method ===(uri)
754
- # Alias for to_r.=== . Tests whether this template matches a given uri.
755
- # @return TrueClass, FalseClass
756
- def_delegators :to_r, :===
757
-
758
- # @method match(uri)
759
- # Alias for to_r.match . Matches this template against the given uri.
760
- # @yield MatchData
761
- # @return MatchData, Object
762
- def_delegators :to_r, :match
763
-
764
- # The type of this template.
765
- #
766
- # @example
767
- # tpl1 = URITemplate::Draft7.new('/foo')
768
- # tpl2 = URITemplate.new( tpl1.pattern, tpl1.type )
769
- # tpl1 == tpl2 #=> true
770
- #
771
- # @see {URITemplate#type}
772
- def type
773
- :draft7
774
- end
775
-
776
- # Returns the level of this template according to the draft ( http://tools.ietf.org/html/draft-gregorio-uritemplate-07#section-1.2 ). Higher level means higher complexity.
777
- # Basically this is defined as:
778
- #
779
- # * Level 1: no operators, one variable per expansion, no variable modifiers
780
- # * Level 2: '+' and '#' operators, one variable per expansion, no variable modifiers
781
- # * Level 3: all operators, multiple variables per expansion, no variable modifiers
782
- # * Level 4: all operators, multiple variables per expansion, all variable modifiers
783
- #
784
- # @example
785
- # URITemplate::Draft7.new('/foo/').level #=> 1
786
- # URITemplate::Draft7.new('/foo{bar}').level #=> 1
787
- # URITemplate::Draft7.new('/foo{#bar}').level #=> 2
788
- # URITemplate::Draft7.new('/foo{.bar}').level #=> 3
789
- # URITemplate::Draft7.new('/foo{bar,baz}').level #=> 3
790
- # URITemplate::Draft7.new('/foo{bar:20}').level #=> 4
791
- # URITemplate::Draft7.new('/foo{bar*}').level #=> 4
792
- #
793
- # Templates of lower levels might be convertible to other formats while templates of higher levels might be incompatible. Level 1 for example should be convertible to any other format since it just contains simple expansions.
794
- #
795
- def level
796
- tokens.map(&:level).max
797
- end
798
-
799
- # Tries to concatenate two templates, as if they were path segments.
800
- # Removes double slashes or insert one if they are missing.
801
- #
802
- # @example
803
- # tpl = URITemplate::Draft7.new('/xy/')
804
- # (tpl / '/z/' ).pattern #=> '/xy/z/'
805
- # (tpl / 'z/' ).pattern #=> '/xy/z/'
806
- # (tpl / '{/z}' ).pattern #=> '/xy{/z}'
807
- # (tpl / 'a' / 'b' ).pattern #=> '/xy/a/b'
808
- #
809
- def /(o)
810
- this, other, this_converted, _ = URITemplate.coerce( self, o )
811
- if this_converted
812
- return this / other
813
- end
814
-
815
- if other.absolute?
816
- raise ArgumentError, "Expected to receive a relative template but got an absoulte one: #{other.inspect}. If you think this is a bug, please report it."
817
- end
818
-
819
- if other.pattern == ''
820
- return self
821
- end
822
- # Merge!
823
- # Analyze the last token of this an the first token of the next and try to merge them
824
- if self.tokens.last.kind_of?(Literal)
825
- if self.tokens.last.string[-1] == SLASH # the last token ends with an /
826
- if other.tokens.first.kind_of? Literal
827
- # both seems to be paths, merge them!
828
- if other.tokens.first.string[0] == SLASH
829
- # strip one '/'
830
- return self.class.new( self.tokens[0..-2] + [ Literal.new(self.tokens.last.string + other.tokens.first.string[1..-1]) ] + other.tokens[1..-1] )
831
- else
832
- # no problem, but we can merge them
833
- return self.class.new( self.tokens[0..-2] + [ Literal.new(self.tokens.last.string + other.tokens.first.string) ] + other.tokens[1..-1] )
834
- end
835
- elsif other.tokens.first.kind_of? Expression::Path
836
- # this will automatically insert '/'
837
- # so we can strip one '/'
838
- return self.class.new( self.tokens[0..-2] + [ Literal.new(self.tokens.last.string[0..-2]) ] + other.tokens )
839
- end
840
- elsif other.tokens.first.kind_of? Literal
841
- # okay, this template does not end with /, but the next starts with a literal => merge them!
842
- if other.tokens.first.string[0] == SLASH
843
- return self.class.new( self.tokens[0..-2] + [Literal.new(self.tokens.last.string + other.tokens.first.string)] + other.tokens[1..-1] )
844
- else
845
- return self.class.new( self.tokens[0..-2] + [Literal.new(self.tokens.last.string + '/' + other.tokens.first.string)] + other.tokens[1..-1] )
846
- end
847
- end
848
- end
849
-
850
- if other.tokens.first.kind_of?(Literal)
851
- if other.tokens.first.string[0] == SLASH
852
- return self.class.new( self.tokens + other.tokens )
853
- else
854
- return self.class.new( self.tokens + [Literal.new('/' + other.tokens.first.string)]+ other.tokens[1..-1] )
855
- end
856
- elsif other.tokens.first.kind_of?(Expression::Path)
857
- return self.class.new( self.tokens + other.tokens )
858
- else
859
- return self.class.new( self.tokens + [Literal.new('/')] + other.tokens )
860
- end
861
- end
862
-
863
- # Returns an array containing a the template tokens.
864
- def tokens
865
- @tokens ||= tokenize!
866
- end
867
-
868
- protected
869
- # @private
870
- def tokenize!
871
- Tokenizer.new(pattern).to_a
872
- end
873
-
874
- def arity
875
- @arity ||= tokens.inject(0){|a,t| a + t.arity }
876
- end
877
-
878
- # @private
879
- def extract_matchdata(matchdata, post_processing)
880
- bc = 1
881
- vars = []
882
- tokens.each{|part|
883
- next if part.literal?
884
- i = 0
885
- pa = part.arity
886
- while i < pa
887
- vars << part.extract(i, matchdata[bc])
888
- bc += 1
889
- i += 1
890
- end
891
- }
892
- if post_processing.include? :convert_result
893
- if post_processing.include? :convert_values
894
- vars.flatten!(1)
895
- return Hash[*vars.map!{|k,v| [k,Utils.pair_array_to_hash(v)] }.flatten(1) ]
896
- else
897
- vars.flatten!(2)
898
- return Hash[*vars]
899
- end
900
- else
901
- if post_processing.include? :convert_value
902
- vars.flatten!(1)
903
- return vars.collect{|k,v| [k,Utils.pair_array_to_hash(v)] }
904
- else
905
- return vars.flatten(1)
906
- end
907
- end
908
- end
909
-
910
266
  end