addressable 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -1,3 +1,17 @@
1
+ === Addressable 2.1.0
2
+ * refactored URI template support out into its own class
3
+ * removed extract method due to being useless and unreliable
4
+ * removed Addressable::URI.expand_template
5
+ * removed Addressable::URI#extract_mapping
6
+ * added partial template expansion
7
+ * fixed minor bugs in the parse and heuristic_parse methods
8
+ * fixed incompatibility with Ruby 1.9.1
9
+ * fixed bottleneck in Addressable::URI#hash and Addressable::URI#to_s
10
+ * fixed unicode normalization exception
11
+ * updated query_values methods to better handle subscript notation
12
+ * worked around issue with freezing URIs
13
+ * improved specs
14
+
1
15
  === Addressable 2.0.2
2
16
  * fixed issue with URI template expansion
3
17
  * fixed issue with percent escaping characters 0-15
data/README CHANGED
@@ -1,10 +1,11 @@
1
1
  Addressable is a replacement for the URI implementation that is part of
2
2
  Ruby's standard library. It more closely conforms to the relevant RFCs and
3
- adds support for IRIs and URI templates.
3
+ adds support for IRIs and URI templates. Additionally, it provides extensive
4
+ support for URI templates.
4
5
 
5
6
  Example usage:
6
7
 
7
- require 'addressable/uri'
8
+ require "addressable/uri"
8
9
 
9
10
  uri = Addressable::URI.parse("http://example.com/path/to/resource/")
10
11
  uri.scheme
@@ -18,16 +19,27 @@ Example usage:
18
19
  uri.normalize
19
20
  #=> #<Addressable::URI:0xc9a4c8 URI:http://www.xn--8ws00zhy3a.com/>
20
21
 
21
- Addressable::URI.expand_template("http://example.com/{-list|+|query}/", {
22
+ require "addressable/template"
23
+
24
+ template = Addressable::Template.new("http://example.com/{-list|+|query}/")
25
+ template.expand({
22
26
  "query" => "an example query".split(" ")
23
27
  })
24
28
  #=> #<Addressable::URI:0xc9d95c URI:http://example.com/an+example+query/>
25
29
 
26
- Addressable::URI.parse(
27
- "http://example.com/a/b/c/?one=1&two=2#foo"
28
- ).extract_mapping(
30
+ template = Addressable::Template.new(
31
+ "http://example.com/{-join|&|one,two,three}/"
32
+ )
33
+ template.partial_expand({"one" => "1", "three" => 3}).pattern
34
+ #=> "http://example.com/?one=1{-prefix|&two=|two}&three=3"
35
+
36
+ template = Addressable::Template.new(
29
37
  "http://{host}/{-suffix|/|segments}?{-join|&|one,two,bogus}\#{fragment}"
30
38
  )
39
+ uri = Addressable::URI.parse(
40
+ "http://example.com/a/b/c/?one=1&two=2#foo"
41
+ )
42
+ template.extract(uri)
31
43
  #=>
32
44
  # {
33
45
  # "host" => "example.com",
data/Rakefile CHANGED
@@ -8,7 +8,6 @@ require 'rake/testtask'
8
8
  require 'rake/rdoctask'
9
9
  require 'rake/packagetask'
10
10
  require 'rake/gempackagetask'
11
- require 'rake/contrib/rubyforgepublisher'
12
11
  require 'spec/rake/spectask'
13
12
 
14
13
  require File.join(File.dirname(__FILE__), 'lib/addressable', 'version')
@@ -52,6 +52,9 @@ module Addressable
52
52
  if input =~ UTF8_REGEX && input =~ UTF8_REGEX_MULTIBYTE
53
53
  parts = unicode_downcase(input).split('.')
54
54
  parts.map! do |part|
55
+ if part.respond_to?(:force_encoding)
56
+ part.force_encoding(Encoding::ASCII_8BIT)
57
+ end
55
58
  if part =~ UTF8_REGEX && part =~ UTF8_REGEX_MULTIBYTE
56
59
  ACE_PREFIX + punycode_encode(unicode_normalize_kc(part))
57
60
  else
@@ -149,33 +152,34 @@ module Addressable
149
152
 
150
153
  p = []
151
154
  ucs4_to_utf8 = lambda do |ch|
155
+ # For some reason, rcov likes to drop BUS errors here.
152
156
  if ch < 128
153
157
  p << ch
154
158
  elsif ch < 2048
155
- p << ((ch >> 6) | 192)
156
- p << ((ch & 63) | 128)
159
+ p << (ch >> 6 | 192)
160
+ p << (ch & 63 | 128)
157
161
  elsif ch < 0x10000
158
- p << ((ch >> 12) | 224)
159
- p << (((ch >> 6) & 63) | 128)
160
- p << ((ch & 63) | 128)
162
+ p << (ch >> 12 | 224)
163
+ p << (ch >> 6 & 63 | 128)
164
+ p << (ch & 63 | 128)
161
165
  elsif ch < 0x200000
162
- p << ((ch >> 18) | 240)
163
- p << (((ch >> 12) & 63) | 128)
164
- p << (((ch >> 6) & 63) | 128)
165
- p << ((ch & 63) | 128)
166
+ p << (ch >> 18 | 240)
167
+ p << (ch >> 12 & 63 | 128)
168
+ p << (ch >> 6 & 63 | 128)
169
+ p << (ch & 63 | 128)
166
170
  elsif ch < 0x4000000
167
- p << ((ch >> 24) | 248)
168
- p << (((ch >> 18) & 63) | 128)
169
- p << (((ch >> 12) & 63) | 128)
170
- p << (((ch >> 6) & 63) | 128)
171
- p << ((ch & 63) | 128)
171
+ p << (ch >> 24 | 248)
172
+ p << (ch >> 18 & 63 | 128)
173
+ p << (ch >> 12 & 63 | 128)
174
+ p << (ch >> 6 & 63 | 128)
175
+ p << (ch & 63 | 128)
172
176
  elsif ch < 0x80000000
173
- p << ((ch >> 30) | 252)
174
- p << (((ch >> 24) & 63) | 128)
175
- p << (((ch >> 18) & 63) | 128)
176
- p << (((ch >> 12) & 63) | 128)
177
- p << (((ch >> 6) & 63) | 128)
178
- p << ((ch & 63) | 128)
177
+ p << (ch >> 30 | 252)
178
+ p << (ch >> 24 & 63 | 128)
179
+ p << (ch >> 18 & 63 | 128)
180
+ p << (ch >> 12 & 63 | 128)
181
+ p << (ch >> 6 & 63 | 128)
182
+ p << (ch & 63 | 128)
179
183
  end
180
184
  end
181
185
 
@@ -0,0 +1,1024 @@
1
+ # encoding:utf-8
2
+ #--
3
+ # Addressable, Copyright (c) 2006-2008 Bob Aman
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ #++
24
+
25
+ $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '/..')))
26
+ $:.uniq!
27
+
28
+ require "addressable/version"
29
+ require "addressable/uri"
30
+
31
+ module Addressable
32
+ ##
33
+ # This is an implementation of a URI template based on
34
+ # <a href="http://tinyurl.com/uritemplatedraft03">URI Template draft 03</a>.
35
+ class Template
36
+ # Constants used throughout the template code.
37
+ anything =
38
+ Addressable::URI::CharacterClasses::RESERVED +
39
+ Addressable::URI::CharacterClasses::UNRESERVED
40
+ OPERATOR_EXPANSION =
41
+ /\{-([a-zA-Z]+)\|([#{anything}]+)\|([#{anything}]+)\}/
42
+ VARIABLE_EXPANSION = /\{([#{anything}]+?)(=([#{anything}]+))?\}/
43
+
44
+ ##
45
+ # Raised if an invalid template value is supplied.
46
+ class InvalidTemplateValueError < StandardError
47
+ end
48
+
49
+ ##
50
+ # Raised if an invalid template operator is used in a pattern.
51
+ class InvalidTemplateOperatorError < StandardError
52
+ end
53
+
54
+ ##
55
+ # Raised if an invalid template operator is used in a pattern.
56
+ class TemplateOperatorAbortedError < StandardError
57
+ end
58
+
59
+ ##
60
+ # This class represents the data that is extracted when a Template
61
+ # is matched against a URI.
62
+ class MatchData
63
+ ##
64
+ # Creates a new MatchData object.
65
+ # MatchData objects should never be instantiated directly.
66
+ #
67
+ # @param [Addressable::URI] uri
68
+ # The URI that the template was matched against.
69
+ def initialize(uri, template, mapping) # :nodoc:
70
+ @uri = uri.dup.freeze
71
+ @template = template
72
+ @mapping = mapping.dup.freeze
73
+ end
74
+
75
+ ##
76
+ # @return [Addressable::URI]
77
+ # The URI that the Template was matched against.
78
+ attr_reader :uri
79
+
80
+ ##
81
+ # @return [Addressable::Template]
82
+ # The Template used for the match.
83
+ attr_reader :template
84
+
85
+ ##
86
+ # @return [Hash]
87
+ # The mapping that resulted from the match.
88
+ # Note that this mapping does not include keys or values for
89
+ # variables that appear in the Template, but are not present
90
+ # in the URI.
91
+ attr_reader :mapping
92
+
93
+ ##
94
+ # @return [Array]
95
+ # The list of variables that were present in the Template.
96
+ # Note that this list will include variables which do not appear
97
+ # in the mapping because they were not present in URI.
98
+ def variables
99
+ self.template.variables
100
+ end
101
+ alias_method :keys, :variables
102
+
103
+ ##
104
+ # @return [Array]
105
+ # The list of values that were captured by the Template.
106
+ # Note that this list will include nils for any variables which
107
+ # were in the Template, but did not appear in the URI.
108
+ def values
109
+ @values ||= self.variables.inject([]) do |accu, key|
110
+ accu << self.mapping[key]
111
+ accu
112
+ end
113
+ end
114
+ alias_method :captures, :values
115
+
116
+ ##
117
+ # Returns a <tt>String</tt> representation of the MatchData's state.
118
+ #
119
+ # @return [String] The MatchData's state, as a <tt>String</tt>.
120
+ def inspect
121
+ sprintf("#<%s:%#0x RESULT:%s>",
122
+ self.class.to_s, self.object_id, self.mapping.inspect)
123
+ end
124
+ end
125
+
126
+ ##
127
+ # Creates a new <tt>Addressable::Template</tt> object.
128
+ #
129
+ # @param [#to_str] pattern The URI Template pattern.
130
+ #
131
+ # @return [Addressable::Template] The initialized Template object.
132
+ def initialize(pattern)
133
+ if !pattern.respond_to?(:to_str)
134
+ raise TypeError, "Can't convert #{pattern.class} into String."
135
+ end
136
+ @pattern = pattern.to_str.freeze
137
+ end
138
+
139
+ ##
140
+ # @return [String] The Template object's pattern.
141
+ attr_reader :pattern
142
+
143
+ ##
144
+ # Returns a <tt>String</tt> representation of the Template object's state.
145
+ #
146
+ # @return [String] The Template object's state, as a <tt>String</tt>.
147
+ def inspect
148
+ sprintf("#<%s:%#0x PATTERN:%s>",
149
+ self.class.to_s, self.object_id, self.pattern)
150
+ end
151
+
152
+ ##
153
+ # Extracts a mapping from the URI using a URI Template pattern.
154
+ #
155
+ # @param [Addressable::URI, #to_str] uri
156
+ # The URI to extract from.
157
+ # @param [#restore, #match] processor
158
+ # A template processor object may optionally be supplied.
159
+ # The object should respond to either the <tt>restore</tt> or
160
+ # <tt>match</tt> messages or both. The <tt>restore</tt> method should
161
+ # take two parameters: [String] name and [String] value. The
162
+ # <tt>restore</tt> method should reverse any transformations that have
163
+ # been performed on the value to ensure a valid URI. The
164
+ # <tt>match</tt> method should take a single parameter: [String] name.
165
+ # The <tt>match</tt> method should return a <tt>String</tt> containing
166
+ # a regular expression capture group for matching on that particular
167
+ # variable. The default value is ".*?". The <tt>match</tt> method has
168
+ # no effect on multivariate operator expansions.
169
+ # @return [Hash, NilClass]
170
+ # The <tt>Hash</tt> mapping that was extracted from the URI, or
171
+ # <tt>nil</tt> if the URI didn't match the template.
172
+ #
173
+ # @example
174
+ # class ExampleProcessor
175
+ # def self.restore(name, value)
176
+ # return value.gsub(/\+/, " ") if name == "query"
177
+ # return value
178
+ # end
179
+ #
180
+ # def self.match(name)
181
+ # return ".*?" if name == "first"
182
+ # return ".*"
183
+ # end
184
+ # end
185
+ #
186
+ # uri = Addressable::URI.parse(
187
+ # "http://example.com/search/an+example+search+query/"
188
+ # )
189
+ # Addressable::Template.new(
190
+ # "http://example.com/search/{query}/"
191
+ # ).extract(uri, ExampleProcessor)
192
+ # #=> {"query" => "an example search query"}
193
+ #
194
+ # uri = Addressable::URI.parse("http://example.com/a/b/c/")
195
+ # Addressable::Template.new(
196
+ # "http://example.com/{first}/{second}/"
197
+ # ).extract(uri, ExampleProcessor)
198
+ # #=> {"first" => "a", "second" => "b/c"}
199
+ #
200
+ # uri = Addressable::URI.parse("http://example.com/a/b/c/")
201
+ # Addressable::Template.new(
202
+ # "http://example.com/{first}/{-list|/|second}/"
203
+ # ).extract(uri)
204
+ # #=> {"first" => "a", "second" => ["b", "c"]}
205
+ def extract(uri, processor=nil)
206
+ match_data = self.match(uri, processor)
207
+ return (match_data ? match_data.mapping : nil)
208
+ end
209
+
210
+ ##
211
+ # Extracts match data from the URI using a URI Template pattern.
212
+ #
213
+ # @param [Addressable::URI, #to_str] uri
214
+ # The URI to extract from.
215
+ # @param [#restore, #match] processor
216
+ # A template processor object may optionally be supplied.
217
+ # The object should respond to either the <tt>restore</tt> or
218
+ # <tt>match</tt> messages or both. The <tt>restore</tt> method should
219
+ # take two parameters: [String] name and [String] value. The
220
+ # <tt>restore</tt> method should reverse any transformations that have
221
+ # been performed on the value to ensure a valid URI. The
222
+ # <tt>match</tt> method should take a single parameter: [String] name.
223
+ # The <tt>match</tt> method should return a <tt>String</tt> containing
224
+ # a regular expression capture group for matching on that particular
225
+ # variable. The default value is ".*?". The <tt>match</tt> method has
226
+ # no effect on multivariate operator expansions.
227
+ # @return [Hash, NilClass]
228
+ # The <tt>Hash</tt> mapping that was extracted from the URI, or
229
+ # <tt>nil</tt> if the URI didn't match the template.
230
+ #
231
+ # @example
232
+ # class ExampleProcessor
233
+ # def self.restore(name, value)
234
+ # return value.gsub(/\+/, " ") if name == "query"
235
+ # return value
236
+ # end
237
+ #
238
+ # def self.match(name)
239
+ # return ".*?" if name == "first"
240
+ # return ".*"
241
+ # end
242
+ # end
243
+ #
244
+ # uri = Addressable::URI.parse(
245
+ # "http://example.com/search/an+example+search+query/"
246
+ # )
247
+ # match = Addressable::Template.new(
248
+ # "http://example.com/search/{query}/"
249
+ # ).match(uri, ExampleProcessor)
250
+ # match.variables
251
+ # #=> ["query"]
252
+ # match.captures
253
+ # #=> ["an example search query"]
254
+ #
255
+ # uri = Addressable::URI.parse("http://example.com/a/b/c/")
256
+ # match = Addressable::Template.new(
257
+ # "http://example.com/{first}/{second}/"
258
+ # ).match(uri, ExampleProcessor)
259
+ # match.variables
260
+ # #=> ["first", "second"]
261
+ # match.captures
262
+ # #=> ["a", "b/c"]
263
+ #
264
+ # uri = Addressable::URI.parse("http://example.com/a/b/c/")
265
+ # match = Addressable::Template.new(
266
+ # "http://example.com/{first}/{-list|/|second}/"
267
+ # ).match(uri)
268
+ # match.variables
269
+ # #=> ["first", "second"]
270
+ # match.captures
271
+ # #=> ["a", ["b", "c"]]
272
+ def match(uri, processor=nil)
273
+ uri = Addressable::URI.parse(uri)
274
+ mapping = {}
275
+
276
+ # First, we need to process the pattern, and extract the values.
277
+ expansions, expansion_regexp =
278
+ parse_template_pattern(pattern, processor)
279
+ unparsed_values = uri.to_str.scan(expansion_regexp).flatten
280
+
281
+ if uri.to_str == pattern
282
+ return Addressable::Template::MatchData.new(uri, self, mapping)
283
+ elsif expansions.size > 0 && expansions.size == unparsed_values.size
284
+ expansions.each_with_index do |expansion, index|
285
+ unparsed_value = unparsed_values[index]
286
+ if expansion =~ OPERATOR_EXPANSION
287
+ operator, argument, variables =
288
+ parse_template_expansion(expansion)
289
+ extract_method = "extract_#{operator}_operator"
290
+ if ([extract_method, extract_method.to_sym] &
291
+ private_methods).empty?
292
+ raise InvalidTemplateOperatorError,
293
+ "Invalid template operator: #{operator}"
294
+ else
295
+ begin
296
+ send(
297
+ extract_method.to_sym, unparsed_value, processor,
298
+ argument, variables, mapping
299
+ )
300
+ rescue TemplateOperatorAbortedError
301
+ return nil
302
+ end
303
+ end
304
+ else
305
+ name = expansion[VARIABLE_EXPANSION, 1]
306
+ value = unparsed_value
307
+ if processor != nil && processor.respond_to?(:restore)
308
+ value = processor.restore(name, value)
309
+ end
310
+ if mapping[name] == nil || mapping[name] == value
311
+ mapping[name] = value
312
+ else
313
+ return nil
314
+ end
315
+ end
316
+ end
317
+ return Addressable::Template::MatchData.new(uri, self, mapping)
318
+ else
319
+ return nil
320
+ end
321
+ end
322
+
323
+ ##
324
+ # Expands a URI template into another URI template.
325
+ #
326
+ # @param [Hash] mapping The mapping that corresponds to the pattern.
327
+ # @param [#validate, #transform] processor
328
+ # An optional processor object may be supplied. The object should
329
+ # respond to either the <tt>validate</tt> or <tt>transform</tt> messages
330
+ # or both. Both the <tt>validate</tt> and <tt>transform</tt> methods
331
+ # should take two parameters: <tt>name</tt> and <tt>value</tt>. The
332
+ # <tt>validate</tt> method should return <tt>true</tt> or
333
+ # <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
334
+ # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
335
+ # exception will be raised if the value is invalid. The
336
+ # <tt>transform</tt> method should return the transformed variable
337
+ # value as a <tt>String</tt>. If a <tt>transform</tt> method is used,
338
+ # the value will not be percent encoded automatically. Unicode
339
+ # normalization will be performed both before and after sending the
340
+ # value to the transform method.
341
+ #
342
+ # @return [Addressable::Template] The partially expanded URI template.
343
+ #
344
+ # @example
345
+ # Addressable::Template.new(
346
+ # "http://example.com/{one}/{two}/"
347
+ # ).partial_expand({"one" => "1"}).pattern
348
+ # #=> "http://example.com/1/{two}/"
349
+ #
350
+ # Addressable::Template.new(
351
+ # "http://example.com/search/{-list|+|query}/"
352
+ # ).partial_expand(
353
+ # {"query" => "an example search query".split(" ")}
354
+ # ).pattern
355
+ # #=> "http://example.com/search/an+example+search+query/"
356
+ #
357
+ # Addressable::Template.new(
358
+ # "http://example.com/{-join|&|one,two}/"
359
+ # ).partial_expand({"one" => "1"}).pattern
360
+ # #=> "http://example.com/?one=1{-prefix|&two=|two}"
361
+ #
362
+ # Addressable::Template.new(
363
+ # "http://example.com/{-join|&|one,two,three}/"
364
+ # ).partial_expand({"one" => "1", "three" => 3}).pattern
365
+ # #=> "http://example.com/?one=1{-prefix|&two=|two}&three=3"
366
+ def partial_expand(mapping, processor=nil)
367
+ result = self.pattern.dup
368
+ transformed_mapping = transform_mapping(mapping, processor)
369
+ result.gsub!(
370
+ /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
371
+ ) do |capture|
372
+ if capture =~ OPERATOR_EXPANSION
373
+ operator, argument, variables, default_mapping =
374
+ parse_template_expansion(capture, transformed_mapping)
375
+ expand_method = "expand_#{operator}_operator"
376
+ if ([expand_method, expand_method.to_sym] & private_methods).empty?
377
+ raise InvalidTemplateOperatorError,
378
+ "Invalid template operator: #{operator}"
379
+ else
380
+ send(
381
+ expand_method.to_sym, argument, variables,
382
+ default_mapping, true
383
+ )
384
+ end
385
+ else
386
+ varname, _, vardefault = capture.scan(/^\{(.+?)(=(.*))?\}$/)[0]
387
+ if transformed_mapping[varname]
388
+ transformed_mapping[varname]
389
+ elsif vardefault
390
+ "{#{varname}=#{vardefault}}"
391
+ else
392
+ "{#{varname}}"
393
+ end
394
+ end
395
+ end
396
+ return Addressable::Template.new(result)
397
+ end
398
+
399
+ ##
400
+ # Expands a URI template into a full URI.
401
+ #
402
+ # @param [Hash] mapping The mapping that corresponds to the pattern.
403
+ # @param [#validate, #transform] processor
404
+ # An optional processor object may be supplied. The object should
405
+ # respond to either the <tt>validate</tt> or <tt>transform</tt> messages
406
+ # or both. Both the <tt>validate</tt> and <tt>transform</tt> methods
407
+ # should take two parameters: <tt>name</tt> and <tt>value</tt>. The
408
+ # <tt>validate</tt> method should return <tt>true</tt> or
409
+ # <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
410
+ # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
411
+ # exception will be raised if the value is invalid. The
412
+ # <tt>transform</tt> method should return the transformed variable
413
+ # value as a <tt>String</tt>. If a <tt>transform</tt> method is used,
414
+ # the value will not be percent encoded automatically. Unicode
415
+ # normalization will be performed both before and after sending the
416
+ # value to the transform method.
417
+ #
418
+ # @return [Addressable::URI] The expanded URI template.
419
+ #
420
+ # @example
421
+ # class ExampleProcessor
422
+ # def self.validate(name, value)
423
+ # return !!(value =~ /^[\w ]+$/) if name == "query"
424
+ # return true
425
+ # end
426
+ #
427
+ # def self.transform(name, value)
428
+ # return value.gsub(/ /, "+") if name == "query"
429
+ # return value
430
+ # end
431
+ # end
432
+ #
433
+ # Addressable::Template.new(
434
+ # "http://example.com/search/{query}/"
435
+ # ).expand(
436
+ # {"query" => "an example search query"},
437
+ # ExampleProcessor
438
+ # ).to_str
439
+ # #=> "http://example.com/search/an+example+search+query/"
440
+ #
441
+ # Addressable::Template.new(
442
+ # "http://example.com/search/{-list|+|query}/"
443
+ # ).expand(
444
+ # {"query" => "an example search query".split(" ")}
445
+ # ).to_str
446
+ # #=> "http://example.com/search/an+example+search+query/"
447
+ #
448
+ # Addressable::Template.new(
449
+ # "http://example.com/search/{query}/"
450
+ # ).expand(
451
+ # {"query" => "bogus!"},
452
+ # ExampleProcessor
453
+ # ).to_str
454
+ # #=> Addressable::Template::InvalidTemplateValueError
455
+ def expand(mapping, processor=nil)
456
+ result = self.pattern.dup
457
+ transformed_mapping = transform_mapping(mapping, processor)
458
+ result.gsub!(
459
+ /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
460
+ ) do |capture|
461
+ if capture =~ OPERATOR_EXPANSION
462
+ operator, argument, variables, default_mapping =
463
+ parse_template_expansion(capture, transformed_mapping)
464
+ expand_method = "expand_#{operator}_operator"
465
+ if ([expand_method, expand_method.to_sym] & private_methods).empty?
466
+ raise InvalidTemplateOperatorError,
467
+ "Invalid template operator: #{operator}"
468
+ else
469
+ send(expand_method.to_sym, argument, variables, default_mapping)
470
+ end
471
+ else
472
+ varname, _, vardefault = capture.scan(/^\{(.+?)(=(.*))?\}$/)[0]
473
+ transformed_mapping[varname] || vardefault
474
+ end
475
+ end
476
+ return Addressable::URI.parse(result)
477
+ end
478
+
479
+ ##
480
+ # Returns an Array of variables used within the template pattern.
481
+ # The variables are listed in the Array in the order they appear within
482
+ # the pattern. Multiple occurrences of a variable within a pattern are
483
+ # not represented in this Array.
484
+ #
485
+ # @return [Array] The variables present in the template's pattern.
486
+ def variables
487
+ @variables ||= (begin
488
+ result = []
489
+
490
+ expansions, expansion_regexp = parse_template_pattern(pattern)
491
+ expansions.each do |expansion|
492
+ if expansion =~ OPERATOR_EXPANSION
493
+ _, _, variables, _ = parse_template_expansion(expansion)
494
+ result.concat(variables)
495
+ else
496
+ result << expansion[VARIABLE_EXPANSION, 1]
497
+ end
498
+ end
499
+ result.uniq
500
+ end)
501
+ end
502
+ alias_method :keys, :variables
503
+
504
+ private
505
+ ##
506
+ # Transforms a mapping so that values can be substituted into the
507
+ # template.
508
+ #
509
+ # @param [Hash] mapping The mapping of variables to values.
510
+ # @param [#validate, #transform] processor
511
+ # An optional processor object may be supplied. The object should
512
+ # respond to either the <tt>validate</tt> or <tt>transform</tt> messages
513
+ # or both. Both the <tt>validate</tt> and <tt>transform</tt> methods
514
+ # should take two parameters: <tt>name</tt> and <tt>value</tt>. The
515
+ # <tt>validate</tt> method should return <tt>true</tt> or
516
+ # <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
517
+ # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
518
+ # exception will be raised if the value is invalid. The
519
+ # <tt>transform</tt> method should return the transformed variable
520
+ # value as a <tt>String</tt>. If a <tt>transform</tt> method is used,
521
+ # the value will not be percent encoded automatically. Unicode
522
+ # normalization will be performed both before and after sending the
523
+ # value to the transform method.
524
+ #
525
+ # @return [Hash] The transformed mapping.
526
+ def transform_mapping(mapping, processor=nil)
527
+ return mapping.inject({}) do |accu, pair|
528
+ name, value = pair
529
+ unless value.respond_to?(:to_ary) || value.respond_to?(:to_str)
530
+ raise TypeError,
531
+ "Can't convert #{value.class} into String or Array."
532
+ end
533
+
534
+ value =
535
+ value.respond_to?(:to_ary) ? value.to_ary : value.to_str
536
+ # Handle unicode normalization
537
+ if value.kind_of?(Array)
538
+ value.map! { |val| Addressable::IDNA.unicode_normalize_kc(val) }
539
+ else
540
+ value = Addressable::IDNA.unicode_normalize_kc(value)
541
+ end
542
+
543
+ if processor == nil || !processor.respond_to?(:transform)
544
+ # Handle percent escaping
545
+ if value.kind_of?(Array)
546
+ transformed_value = value.map do |val|
547
+ Addressable::URI.encode_component(
548
+ val, Addressable::URI::CharacterClasses::UNRESERVED)
549
+ end
550
+ else
551
+ transformed_value = Addressable::URI.encode_component(
552
+ value, Addressable::URI::CharacterClasses::UNRESERVED)
553
+ end
554
+ end
555
+
556
+ # Process, if we've got a processor
557
+ if processor != nil
558
+ if processor.respond_to?(:validate)
559
+ if !processor.validate(name, value)
560
+ display_value = value.kind_of?(Array) ? value.inspect : value
561
+ raise InvalidTemplateValueError,
562
+ "#{name}=#{display_value} is an invalid template value."
563
+ end
564
+ end
565
+ if processor.respond_to?(:transform)
566
+ transformed_value = processor.transform(name, value)
567
+ if transformed_value.kind_of?(Array)
568
+ transformed_value.map! do |val|
569
+ Addressable::IDNA.unicode_normalize_kc(val)
570
+ end
571
+ else
572
+ transformed_value =
573
+ Addressable::IDNA.unicode_normalize_kc(transformed_value)
574
+ end
575
+ end
576
+ end
577
+
578
+ accu[name] = transformed_value
579
+ accu
580
+ end
581
+ end
582
+
583
+ ##
584
+ # Expands a URI Template opt operator.
585
+ #
586
+ # @param [String] argument The argument to the operator.
587
+ # @param [Array] variables The variables the operator is working on.
588
+ # @param [Hash] mapping The mapping of variables to values.
589
+ #
590
+ # @return [String] The expanded result.
591
+ def expand_opt_operator(argument, variables, mapping, partial=false)
592
+ variables_present = variables.any? do |variable|
593
+ mapping[variable] != [] &&
594
+ mapping[variable]
595
+ end
596
+ if partial && !variables_present
597
+ "{-opt|#{argument}|#{variables.join(",")}}"
598
+ elsif variables_present
599
+ argument
600
+ else
601
+ ""
602
+ end
603
+ end
604
+
605
+ ##
606
+ # Expands a URI Template neg operator.
607
+ #
608
+ # @param [String] argument The argument to the operator.
609
+ # @param [Array] variables The variables the operator is working on.
610
+ # @param [Hash] mapping The mapping of variables to values.
611
+ #
612
+ # @return [String] The expanded result.
613
+ def expand_neg_operator(argument, variables, mapping, partial=false)
614
+ variables_present = variables.any? do |variable|
615
+ mapping[variable] != [] &&
616
+ mapping[variable]
617
+ end
618
+ if partial && !variables_present
619
+ "{-neg|#{argument}|#{variables.join(",")}}"
620
+ elsif variables_present
621
+ ""
622
+ else
623
+ argument
624
+ end
625
+ end
626
+
627
+ ##
628
+ # Expands a URI Template prefix operator.
629
+ #
630
+ # @param [String] argument The argument to the operator.
631
+ # @param [Array] variables The variables the operator is working on.
632
+ # @param [Hash] mapping The mapping of variables to values.
633
+ #
634
+ # @return [String] The expanded result.
635
+ def expand_prefix_operator(argument, variables, mapping, partial=false)
636
+ if variables.size != 1
637
+ raise InvalidTemplateOperatorError,
638
+ "Template operator 'prefix' takes exactly one variable."
639
+ end
640
+ value = mapping[variables.first]
641
+ if !partial || value
642
+ if value.kind_of?(Array)
643
+ (value.map { |list_value| argument + list_value }).join("")
644
+ elsif value
645
+ argument + value.to_s
646
+ end
647
+ else
648
+ "{-prefix|#{argument}|#{variables.first}}"
649
+ end
650
+ end
651
+
652
+ ##
653
+ # Expands a URI Template suffix operator.
654
+ #
655
+ # @param [String] argument The argument to the operator.
656
+ # @param [Array] variables The variables the operator is working on.
657
+ # @param [Hash] mapping The mapping of variables to values.
658
+ #
659
+ # @return [String] The expanded result.
660
+ def expand_suffix_operator(argument, variables, mapping, partial=false)
661
+ if variables.size != 1
662
+ raise InvalidTemplateOperatorError,
663
+ "Template operator 'suffix' takes exactly one variable."
664
+ end
665
+ value = mapping[variables.first]
666
+ if !partial || value
667
+ if value.kind_of?(Array)
668
+ (value.map { |list_value| list_value + argument }).join("")
669
+ elsif value
670
+ value.to_s + argument
671
+ end
672
+ else
673
+ "{-suffix|#{argument}|#{variables.first}}"
674
+ end
675
+ end
676
+
677
+ ##
678
+ # Expands a URI Template join operator.
679
+ #
680
+ # @param [String] argument The argument to the operator.
681
+ # @param [Array] variables The variables the operator is working on.
682
+ # @param [Hash] mapping The mapping of variables to values.
683
+ #
684
+ # @return [String] The expanded result.
685
+ def expand_join_operator(argument, variables, mapping, partial=false)
686
+ if !partial
687
+ variable_values = variables.inject([]) do |accu, variable|
688
+ if !mapping[variable].kind_of?(Array)
689
+ if mapping[variable]
690
+ accu << variable + "=" + (mapping[variable])
691
+ end
692
+ else
693
+ raise InvalidTemplateOperatorError,
694
+ "Template operator 'join' does not accept Array values."
695
+ end
696
+ accu
697
+ end
698
+ variable_values.join(argument)
699
+ else
700
+ buffer = ""
701
+ state = :suffix
702
+ variables.each_with_index do |variable, index|
703
+ if !mapping[variable].kind_of?(Array)
704
+ if mapping[variable]
705
+ if buffer.empty? || buffer[-1..-1] == "}"
706
+ buffer << (variable + "=" + (mapping[variable]))
707
+ elsif state == :suffix
708
+ buffer << argument
709
+ buffer << (variable + "=" + (mapping[variable]))
710
+ else
711
+ buffer << (variable + "=" + (mapping[variable]))
712
+ end
713
+ else
714
+ if !buffer.empty? && (buffer[-1..-1] != "}" || state == :prefix)
715
+ buffer << "{-opt|#{argument}|#{variable}}"
716
+ state = :prefix
717
+ end
718
+ if buffer.empty? && variables.size == 1
719
+ # Evaluates back to itself
720
+ buffer << "{-join|#{argument}|#{variable}}"
721
+ else
722
+ buffer << "{-prefix|#{variable}=|#{variable}}"
723
+ end
724
+ if (index != (variables.size - 1) && state == :suffix)
725
+ buffer << "{-opt|#{argument}|#{variable}}"
726
+ elsif index != (variables.size - 1) &&
727
+ mapping[variables[index + 1]]
728
+ buffer << argument
729
+ state = :prefix
730
+ end
731
+ end
732
+ else
733
+ raise InvalidTemplateOperatorError,
734
+ "Template operator 'join' does not accept Array values."
735
+ end
736
+ end
737
+ buffer
738
+ end
739
+ end
740
+
741
+ ##
742
+ # Expands a URI Template list operator.
743
+ #
744
+ # @param [String] argument The argument to the operator.
745
+ # @param [Array] variables The variables the operator is working on.
746
+ # @param [Hash] mapping The mapping of variables to values.
747
+ #
748
+ # @return [String] The expanded result.
749
+ def expand_list_operator(argument, variables, mapping, partial=false)
750
+ if variables.size != 1
751
+ raise InvalidTemplateOperatorError,
752
+ "Template operator 'list' takes exactly one variable."
753
+ end
754
+ if !partial || mapping[variables.first]
755
+ values = mapping[variables.first]
756
+ if values
757
+ if values.kind_of?(Array)
758
+ values.join(argument)
759
+ else
760
+ raise InvalidTemplateOperatorError,
761
+ "Template operator 'list' only accepts Array values."
762
+ end
763
+ end
764
+ else
765
+ "{-list|#{argument}|#{variables.first}}"
766
+ end
767
+ end
768
+
769
+ ##
770
+ # Parses a URI template expansion <tt>String</tt>.
771
+ #
772
+ # @param [String] expansion The operator <tt>String</tt>.
773
+ # @param [Hash] mapping An optional mapping to merge defaults into.
774
+ #
775
+ # @return [Array]
776
+ # A tuple of the operator, argument, variables, and mapping.
777
+ def parse_template_expansion(capture, mapping={})
778
+ operator, argument, variables = capture[1...-1].split("|")
779
+ operator.gsub!(/^\-/, "")
780
+ variables = variables.split(",")
781
+ mapping = (variables.inject({}) do |accu, var|
782
+ varname, _, vardefault = var.scan(/^(.+?)(=(.*))?$/)[0]
783
+ accu[varname] = vardefault
784
+ accu
785
+ end).merge(mapping)
786
+ variables = variables.map { |var| var.gsub(/=.*$/, "") }
787
+ return operator, argument, variables, mapping
788
+ end
789
+
790
+ ##
791
+ # Generates the <tt>Regexp</tt> that parses a template pattern.
792
+ #
793
+ # @param [String] pattern The URI template pattern.
794
+ # @param [#match] processor The template processor to use.
795
+ #
796
+ # @return [Regexp]
797
+ # A regular expression which may be used to parse a template pattern.
798
+ def parse_template_pattern(pattern, processor=nil)
799
+ # Escape the pattern. The two gsubs restore the escaped curly braces
800
+ # back to their original form. Basically, escape everything that isn't
801
+ # within an expansion.
802
+ escaped_pattern = Regexp.escape(
803
+ pattern
804
+ ).gsub(/\\\{(.*?)\\\}/) do |escaped|
805
+ escaped.gsub(/\\(.)/, "\\1")
806
+ end
807
+
808
+ expansions = []
809
+
810
+ # Create a regular expression that captures the values of the
811
+ # variables in the URI.
812
+ regexp_string = escaped_pattern.gsub(
813
+ /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
814
+ ) do |expansion|
815
+ expansions << expansion
816
+ if expansion =~ OPERATOR_EXPANSION
817
+ capture_group = "(.*)"
818
+ operator, argument, names, _ =
819
+ parse_template_expansion(expansion)
820
+ if processor != nil && processor.respond_to?(:match)
821
+ # We can only lookup the match values for single variable
822
+ # operator expansions. Besides, ".*" is usually the only
823
+ # reasonable value for multivariate operators anyways.
824
+ if ["prefix", "suffix", "list"].include?(operator)
825
+ capture_group = "(#{processor.match(names.first)})"
826
+ end
827
+ elsif operator == "prefix"
828
+ capture_group = "(#{Regexp.escape(argument)}.*?)"
829
+ elsif operator == "suffix"
830
+ capture_group = "(.*?#{Regexp.escape(argument)})"
831
+ end
832
+ capture_group
833
+ else
834
+ capture_group = "(.*?)"
835
+ if processor != nil && processor.respond_to?(:match)
836
+ name = expansion[/\{([^\}=]+)(=[^\}]+)?\}/, 1]
837
+ capture_group = "(#{processor.match(name)})"
838
+ end
839
+ capture_group
840
+ end
841
+ end
842
+
843
+ # Ensure that the regular expression matches the whole URI.
844
+ regexp_string = "^#{regexp_string}$"
845
+
846
+ return expansions, Regexp.new(regexp_string)
847
+ end
848
+
849
+ ##
850
+ # Extracts a URI Template opt operator.
851
+ #
852
+ # @param [String] value The unparsed value to extract from.
853
+ # @param [#restore] processor The processor object.
854
+ # @param [String] argument The argument to the operator.
855
+ # @param [Array] variables The variables the operator is working on.
856
+ # @param [Hash] mapping The mapping of variables to values.
857
+ #
858
+ # @return [String] The extracted result.
859
+ def extract_opt_operator(
860
+ value, processor, argument, variables, mapping)
861
+ if value != "" && value != argument
862
+ raise TemplateOperatorAbortedError,
863
+ "Value for template operator 'opt' was unexpected."
864
+ end
865
+ end
866
+
867
+ ##
868
+ # Extracts a URI Template neg operator.
869
+ #
870
+ # @param [String] value The unparsed value to extract from.
871
+ # @param [#restore] processor The processor object.
872
+ # @param [String] argument The argument to the operator.
873
+ # @param [Array] variables The variables the operator is working on.
874
+ # @param [Hash] mapping The mapping of variables to values.
875
+ #
876
+ # @return [String] The extracted result.
877
+ def extract_neg_operator(
878
+ value, processor, argument, variables, mapping)
879
+ if value != "" && value != argument
880
+ raise TemplateOperatorAbortedError,
881
+ "Value for template operator 'neg' was unexpected."
882
+ end
883
+ end
884
+
885
+ ##
886
+ # Extracts a URI Template prefix operator.
887
+ #
888
+ # @param [String] value The unparsed value to extract from.
889
+ # @param [#restore] processor The processor object.
890
+ # @param [String] argument The argument to the operator.
891
+ # @param [Array] variables The variables the operator is working on.
892
+ # @param [Hash] mapping The mapping of variables to values.
893
+ #
894
+ # @return [String] The extracted result.
895
+ def extract_prefix_operator(
896
+ value, processor, argument, variables, mapping)
897
+ if variables.size != 1
898
+ raise InvalidTemplateOperatorError,
899
+ "Template operator 'prefix' takes exactly one variable."
900
+ end
901
+ if value[0...argument.size] != argument
902
+ raise TemplateOperatorAbortedError,
903
+ "Value for template operator 'prefix' missing expected prefix."
904
+ end
905
+ values = value.split(argument, -1)
906
+ values << "" if value[-argument.size..-1] == argument
907
+ values.shift if values[0] == ""
908
+ values.pop if values[-1] == ""
909
+
910
+ if processor && processor.respond_to?(:restore)
911
+ values.map! { |value| processor.restore(variables.first, value) }
912
+ end
913
+ values = values.first if values.size == 1
914
+ if mapping[variables.first] == nil || mapping[variables.first] == values
915
+ mapping[variables.first] = values
916
+ else
917
+ raise TemplateOperatorAbortedError,
918
+ "Value mismatch for repeated variable."
919
+ end
920
+ end
921
+
922
+ ##
923
+ # Extracts a URI Template suffix operator.
924
+ #
925
+ # @param [String] value The unparsed value to extract from.
926
+ # @param [#restore] processor The processor object.
927
+ # @param [String] argument The argument to the operator.
928
+ # @param [Array] variables The variables the operator is working on.
929
+ # @param [Hash] mapping The mapping of variables to values.
930
+ #
931
+ # @return [String] The extracted result.
932
+ def extract_suffix_operator(
933
+ value, processor, argument, variables, mapping)
934
+ if variables.size != 1
935
+ raise InvalidTemplateOperatorError,
936
+ "Template operator 'suffix' takes exactly one variable."
937
+ end
938
+ if value[-argument.size..-1] != argument
939
+ raise TemplateOperatorAbortedError,
940
+ "Value for template operator 'suffix' missing expected suffix."
941
+ end
942
+ values = value.split(argument, -1)
943
+ values.pop if values[-1] == ""
944
+ if processor && processor.respond_to?(:restore)
945
+ values.map! { |value| processor.restore(variables.first, value) }
946
+ end
947
+ values = values.first if values.size == 1
948
+ if mapping[variables.first] == nil || mapping[variables.first] == values
949
+ mapping[variables.first] = values
950
+ else
951
+ raise TemplateOperatorAbortedError,
952
+ "Value mismatch for repeated variable."
953
+ end
954
+ end
955
+
956
+ ##
957
+ # Extracts a URI Template join operator.
958
+ #
959
+ # @param [String] value The unparsed value to extract from.
960
+ # @param [#restore] processor The processor object.
961
+ # @param [String] argument The argument to the operator.
962
+ # @param [Array] variables The variables the operator is working on.
963
+ # @param [Hash] mapping The mapping of variables to values.
964
+ #
965
+ # @return [String] The extracted result.
966
+ def extract_join_operator(value, processor, argument, variables, mapping)
967
+ unparsed_values = value.split(argument)
968
+ parsed_variables = []
969
+ for unparsed_value in unparsed_values
970
+ name = unparsed_value[/^(.+?)=(.+)$/, 1]
971
+ parsed_variables << name
972
+ parsed_value = unparsed_value[/^(.+?)=(.+)$/, 2]
973
+ if processor && processor.respond_to?(:restore)
974
+ parsed_value = processor.restore(name, parsed_value)
975
+ end
976
+ if mapping[name] == nil || mapping[name] == parsed_value
977
+ mapping[name] = parsed_value
978
+ else
979
+ raise TemplateOperatorAbortedError,
980
+ "Value mismatch for repeated variable."
981
+ end
982
+ end
983
+ for variable in variables
984
+ if !parsed_variables.include?(variable) && mapping[variable] != nil
985
+ raise TemplateOperatorAbortedError,
986
+ "Value mismatch for repeated variable."
987
+ end
988
+ end
989
+ if (parsed_variables & variables) != parsed_variables
990
+ raise TemplateOperatorAbortedError,
991
+ "Template operator 'join' variable mismatch: " +
992
+ "#{parsed_variables.inspect}, #{variables.inspect}"
993
+ end
994
+ end
995
+
996
+ ##
997
+ # Extracts a URI Template list operator.
998
+ #
999
+ # @param [String] value The unparsed value to extract from.
1000
+ # @param [#restore] processor The processor object.
1001
+ # @param [String] argument The argument to the operator.
1002
+ # @param [Array] variables The variables the operator is working on.
1003
+ # @param [Hash] mapping The mapping of variables to values.
1004
+ #
1005
+ # @return [String] The extracted result.
1006
+ def extract_list_operator(value, processor, argument, variables, mapping)
1007
+ if variables.size != 1
1008
+ raise InvalidTemplateOperatorError,
1009
+ "Template operator 'list' takes exactly one variable."
1010
+ end
1011
+ values = value.split(argument, -1)
1012
+ values.pop if values[-1] == ""
1013
+ if processor && processor.respond_to?(:restore)
1014
+ values.map! { |value| processor.restore(variables.first, value) }
1015
+ end
1016
+ if mapping[variables.first] == nil || mapping[variables.first] == values
1017
+ mapping[variables.first] = values
1018
+ else
1019
+ raise TemplateOperatorAbortedError,
1020
+ "Value mismatch for repeated variable."
1021
+ end
1022
+ end
1023
+ end
1024
+ end