addressable 2.0.2 → 2.1.0

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