addressable 2.2.3 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of addressable might be problematic. Click here for more details.

@@ -1,42 +1,80 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # encoding:utf-8
2
4
  #--
3
- # Addressable, Copyright (c) 2006-2010 Bob Aman
5
+ # Copyright (C) Bob Aman
4
6
  #
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:
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
12
10
  #
13
- # The above copyright notice and this permission notice shall be
14
- # included in all copies or substantial portions of the Software.
11
+ # http://www.apache.org/licenses/LICENSE-2.0
15
12
  #
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.
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
23
18
  #++
24
19
 
20
+
25
21
  require "addressable/version"
26
22
  require "addressable/uri"
27
23
 
28
24
  module Addressable
29
25
  ##
30
26
  # This is an implementation of a URI template based on
31
- # <a href="http://tinyurl.com/uritemplatedraft03">URI Template draft 03</a>.
27
+ # RFC 6570 (http://tools.ietf.org/html/rfc6570).
32
28
  class Template
33
29
  # Constants used throughout the template code.
34
30
  anything =
35
31
  Addressable::URI::CharacterClasses::RESERVED +
36
32
  Addressable::URI::CharacterClasses::UNRESERVED
37
- OPERATOR_EXPANSION =
38
- /\{-([a-zA-Z]+)\|([#{anything}]+)\|([#{anything}]+)\}/
39
- VARIABLE_EXPANSION = /\{([#{anything}]+?)(?:=([#{anything}]+))?\}/
33
+
34
+
35
+ variable_char_class =
36
+ Addressable::URI::CharacterClasses::ALPHA +
37
+ Addressable::URI::CharacterClasses::DIGIT + '_'
38
+
39
+ var_char =
40
+ "(?:(?:[#{variable_char_class}]|%[a-fA-F0-9][a-fA-F0-9])+)"
41
+ RESERVED =
42
+ "(?:[#{anything}]|%[a-fA-F0-9][a-fA-F0-9])"
43
+ UNRESERVED =
44
+ "(?:[#{
45
+ Addressable::URI::CharacterClasses::UNRESERVED
46
+ }]|%[a-fA-F0-9][a-fA-F0-9])"
47
+ variable =
48
+ "(?:#{var_char}(?:\\.?#{var_char})*)"
49
+ varspec =
50
+ "(?:(#{variable})(\\*|:\\d+)?)"
51
+ VARNAME =
52
+ /^#{variable}$/
53
+ VARSPEC =
54
+ /^#{varspec}$/
55
+ VARIABLE_LIST =
56
+ /^#{varspec}(?:,#{varspec})*$/
57
+ operator =
58
+ "+#./;?&=,!@|"
59
+ EXPRESSION =
60
+ /\{([#{operator}])?(#{varspec}(?:,#{varspec})*)\}/
61
+
62
+
63
+ LEADERS = {
64
+ '?' => '?',
65
+ '/' => '/',
66
+ '#' => '#',
67
+ '.' => '.',
68
+ ';' => ';',
69
+ '&' => '&'
70
+ }
71
+ JOINERS = {
72
+ '?' => '&',
73
+ '.' => '.',
74
+ ';' => ';',
75
+ '&' => '&',
76
+ '/' => '/'
77
+ }
40
78
 
41
79
  ##
42
80
  # Raised if an invalid template value is supplied.
@@ -63,7 +101,7 @@ module Addressable
63
101
  #
64
102
  # @param [Addressable::URI] uri
65
103
  # The URI that the template was matched against.
66
- def initialize(uri, template, mapping) # :nodoc:
104
+ def initialize(uri, template, mapping)
67
105
  @uri = uri.dup.freeze
68
106
  @template = template
69
107
  @mapping = mapping.dup.freeze
@@ -96,6 +134,7 @@ module Addressable
96
134
  self.template.variables
97
135
  end
98
136
  alias_method :keys, :variables
137
+ alias_method :names, :variables
99
138
 
100
139
  ##
101
140
  # @return [Array]
@@ -110,6 +149,64 @@ module Addressable
110
149
  end
111
150
  alias_method :captures, :values
112
151
 
152
+ ##
153
+ # Accesses captured values by name or by index.
154
+ #
155
+ # @param [String, Symbol, Fixnum] key
156
+ # Capture index or name. Note that when accessing by with index
157
+ # of 0, the full URI will be returned. The intention is to mimic
158
+ # the ::MatchData#[] behavior.
159
+ #
160
+ # @param [#to_int, nil] len
161
+ # If provided, an array of values will be returend with the given
162
+ # parameter used as length.
163
+ #
164
+ # @return [Array, String, nil]
165
+ # The captured value corresponding to the index or name. If the
166
+ # value was not provided or the key is unknown, nil will be
167
+ # returned.
168
+ #
169
+ # If the second parameter is provided, an array of that length will
170
+ # be returned instead.
171
+ def [](key, len = nil)
172
+ if len
173
+ to_a[key, len]
174
+ elsif String === key or Symbol === key
175
+ mapping[key.to_s]
176
+ else
177
+ to_a[key]
178
+ end
179
+ end
180
+
181
+ ##
182
+ # @return [Array]
183
+ # Array with the matched URI as first element followed by the captured
184
+ # values.
185
+ def to_a
186
+ [to_s, *values]
187
+ end
188
+
189
+ ##
190
+ # @return [String]
191
+ # The matched URI as String.
192
+ def to_s
193
+ uri.to_s
194
+ end
195
+ alias_method :string, :to_s
196
+
197
+ # Returns multiple captured values at once.
198
+ #
199
+ # @param [String, Symbol, Fixnum] *indexes
200
+ # Indices of the captures to be returned
201
+ #
202
+ # @return [Array]
203
+ # Values corresponding to given indices.
204
+ #
205
+ # @see Addressable::Template::MatchData#[]
206
+ def values_at(*indexes)
207
+ indexes.map { |i| self[i] }
208
+ end
209
+
113
210
  ##
114
211
  # Returns a <tt>String</tt> representation of the MatchData's state.
115
212
  #
@@ -118,6 +215,15 @@ module Addressable
118
215
  sprintf("#<%s:%#0x RESULT:%s>",
119
216
  self.class.to_s, self.object_id, self.mapping.inspect)
120
217
  end
218
+
219
+ ##
220
+ # Dummy method for code expecting a ::MatchData instance
221
+ #
222
+ # @return [String] An empty string.
223
+ def pre_match
224
+ ""
225
+ end
226
+ alias_method :post_match, :pre_match
121
227
  end
122
228
 
123
229
  ##
@@ -130,7 +236,18 @@ module Addressable
130
236
  if !pattern.respond_to?(:to_str)
131
237
  raise TypeError, "Can't convert #{pattern.class} into String."
132
238
  end
133
- @pattern = pattern.to_str.freeze
239
+ @pattern = pattern.to_str.dup.freeze
240
+ end
241
+
242
+ ##
243
+ # Freeze URI, initializing instance variables.
244
+ #
245
+ # @return [Addressable::URI] The frozen URI object.
246
+ def freeze
247
+ self.variables
248
+ self.variable_defaults
249
+ self.named_captures
250
+ super
134
251
  end
135
252
 
136
253
  ##
@@ -146,6 +263,26 @@ module Addressable
146
263
  self.class.to_s, self.object_id, self.pattern)
147
264
  end
148
265
 
266
+ ##
267
+ # Returns <code>true</code> if the Template objects are equal. This method
268
+ # does NOT normalize either Template before doing the comparison.
269
+ #
270
+ # @param [Object] template The Template to compare.
271
+ #
272
+ # @return [TrueClass, FalseClass]
273
+ # <code>true</code> if the Templates are equivalent, <code>false</code>
274
+ # otherwise.
275
+ def ==(template)
276
+ return false unless template.kind_of?(Template)
277
+ return self.pattern == template.pattern
278
+ end
279
+
280
+ ##
281
+ # Addressable::Template makes no distinction between `==` and `eql?`.
282
+ #
283
+ # @see #==
284
+ alias_method :eql?, :==
285
+
149
286
  ##
150
287
  # Extracts a mapping from the URI using a URI Template pattern.
151
288
  #
@@ -155,19 +292,21 @@ module Addressable
155
292
  # @param [#restore, #match] processor
156
293
  # A template processor object may optionally be supplied.
157
294
  #
158
- # The object should respond to either the <tt>restore</tt> or
159
- # <tt>match</tt> messages or both. The <tt>restore</tt> method should take
160
- # two parameters: [String] name and [String] value. The <tt>restore</tt>
161
- # method should reverse any transformations that have been performed on the
162
- # value to ensure a valid URI. The <tt>match</tt> method should take a
163
- # single parameter: [String] name. The <tt>match</tt> method should return
164
- # a <tt>String</tt> containing a regular expression capture group for
165
- # matching on that particular variable. The default value is ".*?". The
166
- # <tt>match</tt> method has no effect on multivariate operator expansions.
295
+ # The object should respond to either the <tt>restore</tt> or
296
+ # <tt>match</tt> messages or both. The <tt>restore</tt> method should
297
+ # take two parameters: `[String] name` and `[String] value`.
298
+ # The <tt>restore</tt> method should reverse any transformations that
299
+ # have been performed on the value to ensure a valid URI.
300
+ # The <tt>match</tt> method should take a single
301
+ # parameter: `[String] name`. The <tt>match</tt> method should return
302
+ # a <tt>String</tt> containing a regular expression capture group for
303
+ # matching on that particular variable. The default value is `".*?"`.
304
+ # The <tt>match</tt> method has no effect on multivariate operator
305
+ # expansions.
167
306
  #
168
307
  # @return [Hash, NilClass]
169
- # The <tt>Hash</tt> mapping that was extracted from the URI, or
170
- # <tt>nil</tt> if the URI didn't match the template.
308
+ # The <tt>Hash</tt> mapping that was extracted from the URI, or
309
+ # <tt>nil</tt> if the URI didn't match the template.
171
310
  #
172
311
  # @example
173
312
  # class ExampleProcessor
@@ -215,19 +354,21 @@ module Addressable
215
354
  # @param [#restore, #match] processor
216
355
  # A template processor object may optionally be supplied.
217
356
  #
218
- # The object should respond to either the <tt>restore</tt> or
219
- # <tt>match</tt> messages or both. The <tt>restore</tt> method should take
220
- # two parameters: [String] name and [String] value. The <tt>restore</tt>
221
- # method should reverse any transformations that have been performed on the
222
- # value to ensure a valid URI. The <tt>match</tt> method should take a
223
- # single parameter: [String] name. The <tt>match</tt> method should return
224
- # a <tt>String</tt> containing a regular expression capture group for
225
- # matching on that particular variable. The default value is ".*?". The
226
- # <tt>match</tt> method has no effect on multivariate operator expansions.
357
+ # The object should respond to either the <tt>restore</tt> or
358
+ # <tt>match</tt> messages or both. The <tt>restore</tt> method should
359
+ # take two parameters: `[String] name` and `[String] value`.
360
+ # The <tt>restore</tt> method should reverse any transformations that
361
+ # have been performed on the value to ensure a valid URI.
362
+ # The <tt>match</tt> method should take a single
363
+ # parameter: `[String] name`. The <tt>match</tt> method should return
364
+ # a <tt>String</tt> containing a regular expression capture group for
365
+ # matching on that particular variable. The default value is `".*?"`.
366
+ # The <tt>match</tt> method has no effect on multivariate operator
367
+ # expansions.
227
368
  #
228
369
  # @return [Hash, NilClass]
229
- # The <tt>Hash</tt> mapping that was extracted from the URI, or
230
- # <tt>nil</tt> if the URI didn't match the template.
370
+ # The <tt>Hash</tt> mapping that was extracted from the URI, or
371
+ # <tt>nil</tt> if the URI didn't match the template.
231
372
  #
232
373
  # @example
233
374
  # class ExampleProcessor
@@ -255,7 +396,7 @@ module Addressable
255
396
  #
256
397
  # uri = Addressable::URI.parse("http://example.com/a/b/c/")
257
398
  # match = Addressable::Template.new(
258
- # "http://example.com/{first}/{second}/"
399
+ # "http://example.com/{first}/{+second}/"
259
400
  # ).match(uri, ExampleProcessor)
260
401
  # match.variables
261
402
  # #=> ["first", "second"]
@@ -264,7 +405,7 @@ module Addressable
264
405
  #
265
406
  # uri = Addressable::URI.parse("http://example.com/a/b/c/")
266
407
  # match = Addressable::Template.new(
267
- # "http://example.com/{first}/{-list|/|second}/"
408
+ # "http://example.com/{first}{/second*}/"
268
409
  # ).match(uri)
269
410
  # match.variables
270
411
  # #=> ["first", "second"]
@@ -277,42 +418,64 @@ module Addressable
277
418
  # First, we need to process the pattern, and extract the values.
278
419
  expansions, expansion_regexp =
279
420
  parse_template_pattern(pattern, processor)
421
+
422
+ return nil unless uri.to_str.match(expansion_regexp)
280
423
  unparsed_values = uri.to_str.scan(expansion_regexp).flatten
281
424
 
282
425
  if uri.to_str == pattern
283
426
  return Addressable::Template::MatchData.new(uri, self, mapping)
284
- elsif expansions.size > 0 && expansions.size == unparsed_values.size
285
- expansions.each_with_index do |expansion, index|
286
- unparsed_value = unparsed_values[index]
287
- if expansion =~ OPERATOR_EXPANSION
288
- operator, argument, variables =
289
- parse_template_expansion(expansion)
290
- extract_method = "extract_#{operator}_operator"
291
- if ([extract_method, extract_method.to_sym] &
292
- private_methods).empty?
293
- raise InvalidTemplateOperatorError,
294
- "Invalid template operator: #{operator}"
295
- else
296
- begin
297
- send(
298
- extract_method.to_sym, unparsed_value, processor,
299
- argument, variables, mapping
300
- )
301
- rescue TemplateOperatorAbortedError
302
- return nil
427
+ elsif expansions.size > 0
428
+ index = 0
429
+ expansions.each do |expansion|
430
+ _, operator, varlist = *expansion.match(EXPRESSION)
431
+ varlist.split(',').each do |varspec|
432
+ _, name, modifier = *varspec.match(VARSPEC)
433
+ mapping[name] ||= nil
434
+ case operator
435
+ when nil, '+', '#', '/', '.'
436
+ unparsed_value = unparsed_values[index]
437
+ name = varspec[VARSPEC, 1]
438
+ value = unparsed_value
439
+ value = value.split(JOINERS[operator]) if value && modifier == '*'
440
+ when ';', '?', '&'
441
+ if modifier == '*'
442
+ if unparsed_values[index]
443
+ value = unparsed_values[index].split(JOINERS[operator])
444
+ value = value.inject({}) do |acc, v|
445
+ key, val = v.split('=')
446
+ val = "" if val.nil?
447
+ acc[key] = val
448
+ acc
449
+ end
450
+ end
451
+ else
452
+ if (unparsed_values[index])
453
+ name, value = unparsed_values[index].split('=')
454
+ value = "" if value.nil?
455
+ end
303
456
  end
304
457
  end
305
- else
306
- name = expansion[VARIABLE_EXPANSION, 1]
307
- value = unparsed_value
308
458
  if processor != nil && processor.respond_to?(:restore)
309
459
  value = processor.restore(name, value)
310
460
  end
311
- if mapping[name] == nil || mapping[name] == value
461
+ if processor == nil
462
+ if value.is_a?(Hash)
463
+ value = value.inject({}){|acc, (k, v)|
464
+ acc[Addressable::URI.unencode_component(k)] =
465
+ Addressable::URI.unencode_component(v)
466
+ acc
467
+ }
468
+ elsif value.is_a?(Array)
469
+ value = value.map{|v| Addressable::URI.unencode_component(v) }
470
+ else
471
+ value = Addressable::URI.unencode_component(value)
472
+ end
473
+ end
474
+ if !mapping.has_key?(name) || mapping[name].nil?
475
+ # Doesn't exist, set to value (even if value is nil)
312
476
  mapping[name] = value
313
- else
314
- return nil
315
477
  end
478
+ index = index + 1
316
479
  end
317
480
  end
318
481
  return Addressable::Template::MatchData.new(uri, self, mapping)
@@ -326,7 +489,9 @@ module Addressable
326
489
  #
327
490
  # @param [Hash] mapping The mapping that corresponds to the pattern.
328
491
  # @param [#validate, #transform] processor
329
- # An optional processor object may be supplied.
492
+ # An optional processor object may be supplied.
493
+ # @param [Boolean] normalize_values
494
+ # Optional flag to enable/disable unicode normalization. Default: true
330
495
  #
331
496
  # The object should respond to either the <tt>validate</tt> or
332
497
  # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
@@ -349,50 +514,19 @@ module Addressable
349
514
  # #=> "http://example.com/1/{two}/"
350
515
  #
351
516
  # Addressable::Template.new(
352
- # "http://example.com/search/{-list|+|query}/"
353
- # ).partial_expand(
354
- # {"query" => "an example search query".split(" ")}
355
- # ).pattern
356
- # #=> "http://example.com/search/an+example+search+query/"
357
- #
358
- # Addressable::Template.new(
359
- # "http://example.com/{-join|&|one,two}/"
517
+ # "http://example.com/{?one,two}/"
360
518
  # ).partial_expand({"one" => "1"}).pattern
361
- # #=> "http://example.com/?one=1{-prefix|&two=|two}"
519
+ # #=> "http://example.com/?one=1{&two}/"
362
520
  #
363
521
  # Addressable::Template.new(
364
- # "http://example.com/{-join|&|one,two,three}/"
522
+ # "http://example.com/{?one,two,three}/"
365
523
  # ).partial_expand({"one" => "1", "three" => 3}).pattern
366
- # #=> "http://example.com/?one=1{-prefix|&two=|two}&three=3"
367
- def partial_expand(mapping, processor=nil)
524
+ # #=> "http://example.com/?one=1{&two}&three=3"
525
+ def partial_expand(mapping, processor=nil, normalize_values=true)
368
526
  result = self.pattern.dup
369
- transformed_mapping = transform_mapping(mapping, processor)
370
- result.gsub!(
371
- /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
372
- ) do |capture|
373
- if capture =~ OPERATOR_EXPANSION
374
- operator, argument, variables, default_mapping =
375
- parse_template_expansion(capture, transformed_mapping)
376
- expand_method = "expand_#{operator}_operator"
377
- if ([expand_method, expand_method.to_sym] & private_methods).empty?
378
- raise InvalidTemplateOperatorError,
379
- "Invalid template operator: #{operator}"
380
- else
381
- send(
382
- expand_method.to_sym, argument, variables,
383
- default_mapping, true
384
- )
385
- end
386
- else
387
- varname, _, vardefault = capture.scan(/^\{(.+?)(=(.*))?\}$/)[0]
388
- if transformed_mapping[varname]
389
- transformed_mapping[varname]
390
- elsif vardefault
391
- "{#{varname}=#{vardefault}}"
392
- else
393
- "{#{varname}}"
394
- end
395
- end
527
+ mapping = normalize_keys(mapping)
528
+ result.gsub!( EXPRESSION ) do |capture|
529
+ transform_partial_capture(mapping, capture, processor, normalize_values)
396
530
  end
397
531
  return Addressable::Template.new(result)
398
532
  end
@@ -403,6 +537,8 @@ module Addressable
403
537
  # @param [Hash] mapping The mapping that corresponds to the pattern.
404
538
  # @param [#validate, #transform] processor
405
539
  # An optional processor object may be supplied.
540
+ # @param [Boolean] normalize_values
541
+ # Optional flag to enable/disable unicode normalization. Default: true
406
542
  #
407
543
  # The object should respond to either the <tt>validate</tt> or
408
544
  # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
@@ -440,11 +576,11 @@ module Addressable
440
576
  # #=> "http://example.com/search/an+example+search+query/"
441
577
  #
442
578
  # Addressable::Template.new(
443
- # "http://example.com/search/{-list|+|query}/"
579
+ # "http://example.com/search/{query}/"
444
580
  # ).expand(
445
- # {"query" => "an example search query".split(" ")}
581
+ # {"query" => "an example search query"}
446
582
  # ).to_str
447
- # #=> "http://example.com/search/an+example+search+query/"
583
+ # #=> "http://example.com/search/an%20example%20search%20query/"
448
584
  #
449
585
  # Addressable::Template.new(
450
586
  # "http://example.com/search/{query}/"
@@ -453,26 +589,11 @@ module Addressable
453
589
  # ExampleProcessor
454
590
  # ).to_str
455
591
  # #=> Addressable::Template::InvalidTemplateValueError
456
- def expand(mapping, processor=nil)
592
+ def expand(mapping, processor=nil, normalize_values=true)
457
593
  result = self.pattern.dup
458
- transformed_mapping = transform_mapping(mapping, processor)
459
- result.gsub!(
460
- /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
461
- ) do |capture|
462
- if capture =~ OPERATOR_EXPANSION
463
- operator, argument, variables, default_mapping =
464
- parse_template_expansion(capture, transformed_mapping)
465
- expand_method = "expand_#{operator}_operator"
466
- if ([expand_method, expand_method.to_sym] & private_methods).empty?
467
- raise InvalidTemplateOperatorError,
468
- "Invalid template operator: #{operator}"
469
- else
470
- send(expand_method.to_sym, argument, variables, default_mapping)
471
- end
472
- else
473
- varname, _, vardefault = capture.scan(/^\{(.+?)(=(.*))?\}$/)[0]
474
- transformed_mapping[varname] || vardefault
475
- end
594
+ mapping = normalize_keys(mapping)
595
+ result.gsub!( EXPRESSION ) do |capture|
596
+ transform_capture(mapping, capture, processor, normalize_values)
476
597
  end
477
598
  return Addressable::URI.parse(result)
478
599
  end
@@ -488,6 +609,7 @@ module Addressable
488
609
  @variables ||= ordered_variable_defaults.map { |var, val| var }.uniq
489
610
  end
490
611
  alias_method :keys, :variables
612
+ alias_method :names, :variables
491
613
 
492
614
  ##
493
615
  # Returns a mapping of variables to their default values specified
@@ -499,31 +621,97 @@ module Addressable
499
621
  Hash[*ordered_variable_defaults.reject { |k, v| v.nil? }.flatten]
500
622
  end
501
623
 
624
+ ##
625
+ # Coerces a template into a `Regexp` object. This regular expression will
626
+ # behave very similarly to the actual template, and should match the same
627
+ # URI values, but it cannot fully handle, for example, values that would
628
+ # extract to an `Array`.
629
+ #
630
+ # @return [Regexp] A regular expression which should match the template.
631
+ def to_regexp
632
+ _, source = parse_template_pattern(pattern)
633
+ Regexp.new(source)
634
+ end
635
+
636
+ ##
637
+ # Returns the source of the coerced `Regexp`.
638
+ #
639
+ # @return [String] The source of the `Regexp` given by {#to_regexp}.
640
+ #
641
+ # @api private
642
+ def source
643
+ self.to_regexp.source
644
+ end
645
+
646
+ ##
647
+ # Returns the named captures of the coerced `Regexp`.
648
+ #
649
+ # @return [Hash] The named captures of the `Regexp` given by {#to_regexp}.
650
+ #
651
+ # @api private
652
+ def named_captures
653
+ self.to_regexp.named_captures
654
+ end
655
+
656
+ ##
657
+ # Generates a route result for a given set of parameters.
658
+ # Should only be used by rack-mount.
659
+ #
660
+ # @param params [Hash] The set of parameters used to expand the template.
661
+ # @param recall [Hash] Default parameters used to expand the template.
662
+ # @param options [Hash] Either a `:processor` or a `:parameterize` block.
663
+ #
664
+ # @api private
665
+ def generate(params={}, recall={}, options={})
666
+ merged = recall.merge(params)
667
+ if options[:processor]
668
+ processor = options[:processor]
669
+ elsif options[:parameterize]
670
+ # TODO: This is sending me into fits trying to shoe-horn this into
671
+ # the existing API. I think I've got this backwards and processors
672
+ # should be a set of 4 optional blocks named :validate, :transform,
673
+ # :match, and :restore. Having to use a singleton here is a huge
674
+ # code smell.
675
+ processor = Object.new
676
+ class <<processor
677
+ attr_accessor :block
678
+ def transform(name, value)
679
+ block.call(name, value)
680
+ end
681
+ end
682
+ processor.block = options[:parameterize]
683
+ else
684
+ processor = nil
685
+ end
686
+ result = self.expand(merged, processor)
687
+ result.to_s if result
688
+ end
689
+
502
690
  private
503
691
  def ordered_variable_defaults
504
- @ordered_variable_defaults ||= (begin
505
- expansions, expansion_regexp = parse_template_pattern(pattern)
506
-
507
- expansions.inject([]) do |result, expansion|
508
- case expansion
509
- when OPERATOR_EXPANSION
510
- _, _, variables, mapping = parse_template_expansion(expansion)
511
- result.concat variables.map { |var| [var, mapping[var]] }
512
- when VARIABLE_EXPANSION
513
- result << [$1, $2]
692
+ @ordered_variable_defaults ||= begin
693
+ expansions, _ = parse_template_pattern(pattern)
694
+ expansions.map do |capture|
695
+ _, _, varlist = *capture.match(EXPRESSION)
696
+ varlist.split(',').map do |varspec|
697
+ varspec[VARSPEC, 1]
514
698
  end
515
- result
516
- end
517
- end)
699
+ end.flatten
700
+ end
518
701
  end
519
702
 
703
+
520
704
  ##
521
- # Transforms a mapping so that values can be substituted into the
522
- # template.
705
+ # Loops through each capture and expands any values available in mapping
523
706
  #
524
- # @param [Hash] mapping The mapping of variables to values.
707
+ # @param [Hash] mapping
708
+ # Set of keys to expand
709
+ # @param [String] capture
710
+ # The expression to expand
525
711
  # @param [#validate, #transform] processor
526
712
  # An optional processor object may be supplied.
713
+ # @param [Boolean] normalize_values
714
+ # Optional flag to enable/disable unicode normalization. Default: true
527
715
  #
528
716
  # The object should respond to either the <tt>validate</tt> or
529
717
  # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
@@ -537,279 +725,252 @@ module Addressable
537
725
  # automatically. Unicode normalization will be performed both before and
538
726
  # after sending the value to the transform method.
539
727
  #
540
- # @return [Hash] The transformed mapping.
541
- def transform_mapping(mapping, processor=nil)
542
- return mapping.inject({}) do |accu, pair|
543
- name, value = pair
544
- value = value.to_s if Numeric === value || Symbol === value
728
+ # @return [String] The expanded expression
729
+ def transform_partial_capture(mapping, capture, processor = nil,
730
+ normalize_values = true)
731
+ _, operator, varlist = *capture.match(EXPRESSION)
545
732
 
546
- unless value.respond_to?(:to_ary) || value.respond_to?(:to_str)
547
- raise TypeError,
548
- "Can't convert #{value.class} into String or Array."
549
- end
733
+ vars = varlist.split(",")
550
734
 
551
- if Symbol === name
552
- name = name.to_s
553
- elsif name.respond_to?(:to_str)
554
- name = name.to_str
555
- else
556
- raise TypeError,
557
- "Can't convert #{name.class} into String."
558
- end
559
- value = value.respond_to?(:to_ary) ? value.to_ary : value.to_str
560
-
561
- # Handle unicode normalization
562
- if value.kind_of?(Array)
563
- value.map! { |val| Addressable::IDNA.unicode_normalize_kc(val) }
564
- else
565
- value = Addressable::IDNA.unicode_normalize_kc(value)
566
- end
567
-
568
- if processor == nil || !processor.respond_to?(:transform)
569
- # Handle percent escaping
570
- if value.kind_of?(Array)
571
- transformed_value = value.map do |val|
572
- Addressable::URI.encode_component(
573
- val, Addressable::URI::CharacterClasses::UNRESERVED)
574
- end
575
- else
576
- transformed_value = Addressable::URI.encode_component(
577
- value, Addressable::URI::CharacterClasses::UNRESERVED)
578
- end
579
- end
580
-
581
- # Process, if we've got a processor
582
- if processor != nil
583
- if processor.respond_to?(:validate)
584
- if !processor.validate(name, value)
585
- display_value = value.kind_of?(Array) ? value.inspect : value
586
- raise InvalidTemplateValueError,
587
- "#{name}=#{display_value} is an invalid template value."
588
- end
589
- end
590
- if processor.respond_to?(:transform)
591
- transformed_value = processor.transform(name, value)
592
- if transformed_value.kind_of?(Array)
593
- transformed_value.map! do |val|
594
- Addressable::IDNA.unicode_normalize_kc(val)
595
- end
596
- else
597
- transformed_value =
598
- Addressable::IDNA.unicode_normalize_kc(transformed_value)
599
- end
600
- end
601
- end
735
+ if operator == "?"
736
+ # partial expansion of form style query variables sometimes requires a
737
+ # slight reordering of the variables to produce a valid url.
738
+ first_to_expand = vars.find { |varspec|
739
+ _, name, _ = *varspec.match(VARSPEC)
740
+ mapping.key?(name) && !mapping[name].nil?
741
+ }
602
742
 
603
- accu[name] = transformed_value
604
- accu
743
+ vars = [first_to_expand] + vars.reject {|varspec| varspec == first_to_expand} if first_to_expand
605
744
  end
606
- end
607
745
 
608
- ##
609
- # Expands a URI Template opt operator.
610
- #
611
- # @param [String] argument The argument to the operator.
612
- # @param [Array] variables The variables the operator is working on.
613
- # @param [Hash] mapping The mapping of variables to values.
614
- #
615
- # @return [String] The expanded result.
616
- def expand_opt_operator(argument, variables, mapping, partial=false)
617
- variables_present = variables.any? do |variable|
618
- mapping[variable] != [] &&
619
- mapping[variable]
620
- end
621
- if partial && !variables_present
622
- "{-opt|#{argument}|#{variables.join(",")}}"
623
- elsif variables_present
624
- argument
625
- else
626
- ""
746
+ vars.
747
+ inject("".dup) do |acc, varspec|
748
+ _, name, _ = *varspec.match(VARSPEC)
749
+ next_val = if mapping.key? name
750
+ transform_capture(mapping, "{#{operator}#{varspec}}",
751
+ processor, normalize_values)
752
+ else
753
+ "{#{operator}#{varspec}}"
754
+ end
755
+ # If we've already expanded at least one '?' operator with non-empty
756
+ # value, change to '&'
757
+ operator = "&" if (operator == "?") && (next_val != "")
758
+ acc << next_val
627
759
  end
628
760
  end
629
761
 
630
762
  ##
631
- # Expands a URI Template neg operator.
763
+ # Transforms a mapped value so that values can be substituted into the
764
+ # template.
632
765
  #
633
- # @param [String] argument The argument to the operator.
634
- # @param [Array] variables The variables the operator is working on.
635
- # @param [Hash] mapping The mapping of variables to values.
766
+ # @param [Hash] mapping The mapping to replace captures
767
+ # @param [String] capture
768
+ # The expression to replace
769
+ # @param [#validate, #transform] processor
770
+ # An optional processor object may be supplied.
771
+ # @param [Boolean] normalize_values
772
+ # Optional flag to enable/disable unicode normalization. Default: true
636
773
  #
637
- # @return [String] The expanded result.
638
- def expand_neg_operator(argument, variables, mapping, partial=false)
639
- variables_present = variables.any? do |variable|
640
- mapping[variable] != [] &&
641
- mapping[variable]
642
- end
643
- if partial && !variables_present
644
- "{-neg|#{argument}|#{variables.join(",")}}"
645
- elsif variables_present
646
- ""
647
- else
648
- argument
649
- end
650
- end
651
-
652
- ##
653
- # Expands a URI Template prefix operator.
654
774
  #
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.
775
+ # The object should respond to either the <tt>validate</tt> or
776
+ # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
777
+ # <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
778
+ # <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
779
+ # or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
780
+ # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt> exception
781
+ # will be raised if the value is invalid. The <tt>transform</tt> method
782
+ # should return the transformed variable value as a <tt>String</tt>. If a
783
+ # <tt>transform</tt> method is used, the value will not be percent encoded
784
+ # automatically. Unicode normalization will be performed both before and
785
+ # after sending the value to the transform method.
658
786
  #
659
- # @return [String] The expanded result.
660
- def expand_prefix_operator(argument, variables, mapping, partial=false)
661
- if variables.size != 1
662
- raise InvalidTemplateOperatorError,
663
- "Template operator 'prefix' 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| argument + list_value }).join("")
669
- elsif value
670
- argument + value.to_s
671
- end
672
- else
673
- "{-prefix|#{argument}|#{variables.first}}"
674
- end
675
- end
787
+ # @return [String] The expanded expression
788
+ def transform_capture(mapping, capture, processor=nil,
789
+ normalize_values=true)
790
+ _, operator, varlist = *capture.match(EXPRESSION)
791
+ return_value = varlist.split(',').inject([]) do |acc, varspec|
792
+ _, name, modifier = *varspec.match(VARSPEC)
793
+ value = mapping[name]
794
+ unless value == nil || value == {}
795
+ allow_reserved = %w(+ #).include?(operator)
796
+ # Common primitives where the .to_s output is well-defined
797
+ if Numeric === value || Symbol === value ||
798
+ value == true || value == false
799
+ value = value.to_s
800
+ end
801
+ length = modifier.gsub(':', '').to_i if modifier =~ /^:\d+/
676
802
 
677
- ##
678
- # Expands a URI Template suffix 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_suffix_operator(argument, variables, mapping, partial=false)
686
- if variables.size != 1
687
- raise InvalidTemplateOperatorError,
688
- "Template operator 'suffix' takes exactly one variable."
689
- end
690
- value = mapping[variables.first]
691
- if !partial || value
692
- if value.kind_of?(Array)
693
- (value.map { |list_value| list_value + argument }).join("")
694
- elsif value
695
- value.to_s + argument
696
- end
697
- else
698
- "{-suffix|#{argument}|#{variables.first}}"
699
- end
700
- end
803
+ unless (Hash === value) ||
804
+ value.respond_to?(:to_ary) || value.respond_to?(:to_str)
805
+ raise TypeError,
806
+ "Can't convert #{value.class} into String or Array."
807
+ end
701
808
 
702
- ##
703
- # Expands a URI Template join operator.
704
- #
705
- # @param [String] argument The argument to the operator.
706
- # @param [Array] variables The variables the operator is working on.
707
- # @param [Hash] mapping The mapping of variables to values.
708
- #
709
- # @return [String] The expanded result.
710
- def expand_join_operator(argument, variables, mapping, partial=false)
711
- if !partial
712
- variable_values = variables.inject([]) do |accu, variable|
713
- if !mapping[variable].kind_of?(Array)
714
- if mapping[variable]
715
- accu << variable + "=" + (mapping[variable])
809
+ value = normalize_value(value) if normalize_values
810
+
811
+ if processor == nil || !processor.respond_to?(:transform)
812
+ # Handle percent escaping
813
+ if allow_reserved
814
+ encode_map =
815
+ Addressable::URI::CharacterClasses::RESERVED +
816
+ Addressable::URI::CharacterClasses::UNRESERVED
817
+ else
818
+ encode_map = Addressable::URI::CharacterClasses::UNRESERVED
716
819
  end
717
- else
718
- raise InvalidTemplateOperatorError,
719
- "Template operator 'join' does not accept Array values."
720
- end
721
- accu
722
- end
723
- variable_values.join(argument)
724
- else
725
- buffer = ""
726
- state = :suffix
727
- variables.each_with_index do |variable, index|
728
- if !mapping[variable].kind_of?(Array)
729
- if mapping[variable]
730
- if buffer.empty? || buffer[-1..-1] == "}"
731
- buffer << (variable + "=" + (mapping[variable]))
732
- elsif state == :suffix
733
- buffer << argument
734
- buffer << (variable + "=" + (mapping[variable]))
735
- else
736
- buffer << (variable + "=" + (mapping[variable]))
820
+ if value.kind_of?(Array)
821
+ transformed_value = value.map do |val|
822
+ if length
823
+ Addressable::URI.encode_component(val[0...length], encode_map)
824
+ else
825
+ Addressable::URI.encode_component(val, encode_map)
826
+ end
737
827
  end
738
- else
739
- if !buffer.empty? && (buffer[-1..-1] != "}" || state == :prefix)
740
- buffer << "{-opt|#{argument}|#{variable}}"
741
- state = :prefix
828
+ unless modifier == "*"
829
+ transformed_value = transformed_value.join(',')
830
+ end
831
+ elsif value.kind_of?(Hash)
832
+ transformed_value = value.map do |key, val|
833
+ if modifier == "*"
834
+ "#{
835
+ Addressable::URI.encode_component( key, encode_map)
836
+ }=#{
837
+ Addressable::URI.encode_component( val, encode_map)
838
+ }"
839
+ else
840
+ "#{
841
+ Addressable::URI.encode_component( key, encode_map)
842
+ },#{
843
+ Addressable::URI.encode_component( val, encode_map)
844
+ }"
845
+ end
742
846
  end
743
- if buffer.empty? && variables.size == 1
744
- # Evaluates back to itself
745
- buffer << "{-join|#{argument}|#{variable}}"
847
+ unless modifier == "*"
848
+ transformed_value = transformed_value.join(',')
849
+ end
850
+ else
851
+ if length
852
+ transformed_value = Addressable::URI.encode_component(
853
+ value[0...length], encode_map)
746
854
  else
747
- buffer << "{-prefix|#{variable}=|#{variable}}"
855
+ transformed_value = Addressable::URI.encode_component(
856
+ value, encode_map)
748
857
  end
749
- if (index != (variables.size - 1) && state == :suffix)
750
- buffer << "{-opt|#{argument}|#{variable}}"
751
- elsif index != (variables.size - 1) &&
752
- mapping[variables[index + 1]]
753
- buffer << argument
754
- state = :prefix
858
+ end
859
+ end
860
+
861
+ # Process, if we've got a processor
862
+ if processor != nil
863
+ if processor.respond_to?(:validate)
864
+ if !processor.validate(name, value)
865
+ display_value = value.kind_of?(Array) ? value.inspect : value
866
+ raise InvalidTemplateValueError,
867
+ "#{name}=#{display_value} is an invalid template value."
868
+ end
869
+ end
870
+ if processor.respond_to?(:transform)
871
+ transformed_value = processor.transform(name, value)
872
+ if normalize_values
873
+ transformed_value = normalize_value(transformed_value)
755
874
  end
756
875
  end
757
- else
758
- raise InvalidTemplateOperatorError,
759
- "Template operator 'join' does not accept Array values."
760
876
  end
877
+ acc << [name, transformed_value]
761
878
  end
762
- buffer
879
+ acc
880
+ end
881
+ return "" if return_value.empty?
882
+ join_values(operator, return_value)
883
+ end
884
+
885
+ ##
886
+ # Takes a set of values, and joins them together based on the
887
+ # operator.
888
+ #
889
+ # @param [String, Nil] operator One of the operators from the set
890
+ # (?,&,+,#,;,/,.), or nil if there wasn't one.
891
+ # @param [Array] return_value
892
+ # The set of return values (as [variable_name, value] tuples) that will
893
+ # be joined together.
894
+ #
895
+ # @return [String] The transformed mapped value
896
+ def join_values(operator, return_value)
897
+ leader = LEADERS.fetch(operator, '')
898
+ joiner = JOINERS.fetch(operator, ',')
899
+ case operator
900
+ when '&', '?'
901
+ leader + return_value.map{|k,v|
902
+ if v.is_a?(Array) && v.first =~ /=/
903
+ v.join(joiner)
904
+ elsif v.is_a?(Array)
905
+ v.map{|inner_value| "#{k}=#{inner_value}"}.join(joiner)
906
+ else
907
+ "#{k}=#{v}"
908
+ end
909
+ }.join(joiner)
910
+ when ';'
911
+ return_value.map{|k,v|
912
+ if v.is_a?(Array) && v.first =~ /=/
913
+ ';' + v.join(";")
914
+ elsif v.is_a?(Array)
915
+ ';' + v.map{|inner_value| "#{k}=#{inner_value}"}.join(";")
916
+ else
917
+ v && v != '' ? ";#{k}=#{v}" : ";#{k}"
918
+ end
919
+ }.join
920
+ else
921
+ leader + return_value.map{|k,v| v}.join(joiner)
763
922
  end
764
923
  end
765
924
 
766
925
  ##
767
- # Expands a URI Template list operator.
926
+ # Takes a set of values, and joins them together based on the
927
+ # operator.
768
928
  #
769
- # @param [String] argument The argument to the operator.
770
- # @param [Array] variables The variables the operator is working on.
771
- # @param [Hash] mapping The mapping of variables to values.
929
+ # @param [Hash, Array, String] value
930
+ # Normalizes keys and values with IDNA#unicode_normalize_kc
772
931
  #
773
- # @return [String] The expanded result.
774
- def expand_list_operator(argument, variables, mapping, partial=false)
775
- if variables.size != 1
776
- raise InvalidTemplateOperatorError,
777
- "Template operator 'list' takes exactly one variable."
932
+ # @return [Hash, Array, String] The normalized values
933
+ def normalize_value(value)
934
+ unless value.is_a?(Hash)
935
+ value = value.respond_to?(:to_ary) ? value.to_ary : value.to_str
778
936
  end
779
- if !partial || mapping[variables.first]
780
- values = mapping[variables.first]
781
- if values
782
- if values.kind_of?(Array)
783
- values.join(argument)
784
- else
785
- raise InvalidTemplateOperatorError,
786
- "Template operator 'list' only accepts Array values."
787
- end
788
- end
937
+
938
+ # Handle unicode normalization
939
+ if value.kind_of?(Array)
940
+ value.map! { |val| Addressable::IDNA.unicode_normalize_kc(val) }
941
+ elsif value.kind_of?(Hash)
942
+ value = value.inject({}) { |acc, (k, v)|
943
+ acc[Addressable::IDNA.unicode_normalize_kc(k)] =
944
+ Addressable::IDNA.unicode_normalize_kc(v)
945
+ acc
946
+ }
789
947
  else
790
- "{-list|#{argument}|#{variables.first}}"
948
+ value = Addressable::IDNA.unicode_normalize_kc(value)
791
949
  end
950
+ value
792
951
  end
793
952
 
794
953
  ##
795
- # Parses a URI template expansion <tt>String</tt>.
954
+ # Generates a hash with string keys
796
955
  #
797
- # @param [String] expansion The operator <tt>String</tt>.
798
- # @param [Hash] mapping An optional mapping to merge defaults into.
956
+ # @param [Hash] mapping A mapping hash to normalize
799
957
  #
800
- # @return [Array]
801
- # A tuple of the operator, argument, variables, and mapping.
802
- def parse_template_expansion(capture, mapping={})
803
- operator, argument, variables = capture[1...-1].split("|", -1)
804
- operator.gsub!(/^\-/, "")
805
- variables = variables.split(",", -1)
806
- mapping = (variables.inject({}) do |accu, var|
807
- varname, _, vardefault = var.scan(/^(.+?)(=(.*))?$/)[0]
808
- accu[varname] = vardefault
958
+ # @return [Hash]
959
+ # A hash with stringified keys
960
+ def normalize_keys(mapping)
961
+ return mapping.inject({}) do |accu, pair|
962
+ name, value = pair
963
+ if Symbol === name
964
+ name = name.to_s
965
+ elsif name.respond_to?(:to_str)
966
+ name = name.to_str
967
+ else
968
+ raise TypeError,
969
+ "Can't convert #{name.class} into String."
970
+ end
971
+ accu[name] = value
809
972
  accu
810
- end).merge(mapping)
811
- variables = variables.map { |var| var.gsub(/=.*$/, "") }
812
- return operator, argument, variables, mapping
973
+ end
813
974
  end
814
975
 
815
976
  ##
@@ -834,216 +995,51 @@ module Addressable
834
995
 
835
996
  # Create a regular expression that captures the values of the
836
997
  # variables in the URI.
837
- regexp_string = escaped_pattern.gsub(
838
- /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
839
- ) do |expansion|
998
+ regexp_string = escaped_pattern.gsub( EXPRESSION ) do |expansion|
999
+
840
1000
  expansions << expansion
841
- if expansion =~ OPERATOR_EXPANSION
842
- capture_group = "(.*)"
843
- operator, argument, names, _ =
844
- parse_template_expansion(expansion)
845
- if processor != nil && processor.respond_to?(:match)
846
- # We can only lookup the match values for single variable
847
- # operator expansions. Besides, ".*" is usually the only
848
- # reasonable value for multivariate operators anyways.
849
- if ["prefix", "suffix", "list"].include?(operator)
850
- capture_group = "(#{processor.match(names.first)})"
1001
+ _, operator, varlist = *expansion.match(EXPRESSION)
1002
+ leader = Regexp.escape(LEADERS.fetch(operator, ''))
1003
+ joiner = Regexp.escape(JOINERS.fetch(operator, ','))
1004
+ combined = varlist.split(',').map do |varspec|
1005
+ _, name, modifier = *varspec.match(VARSPEC)
1006
+
1007
+ result = processor && processor.respond_to?(:match) ? processor.match(name) : nil
1008
+ if result
1009
+ "(?<#{name}>#{ result })"
1010
+ else
1011
+ group = case operator
1012
+ when '+'
1013
+ "#{ RESERVED }*?"
1014
+ when '#'
1015
+ "#{ RESERVED }*?"
1016
+ when '/'
1017
+ "#{ UNRESERVED }*?"
1018
+ when '.'
1019
+ "#{ UNRESERVED.gsub('\.', '') }*?"
1020
+ when ';'
1021
+ "#{ UNRESERVED }*=?#{ UNRESERVED }*?"
1022
+ when '?'
1023
+ "#{ UNRESERVED }*=#{ UNRESERVED }*?"
1024
+ when '&'
1025
+ "#{ UNRESERVED }*=#{ UNRESERVED }*?"
1026
+ else
1027
+ "#{ UNRESERVED }*?"
1028
+ end
1029
+ if modifier == '*'
1030
+ "(?<#{name}>#{group}(?:#{joiner}?#{group})*)?"
1031
+ else
1032
+ "(?<#{name}>#{group})?"
851
1033
  end
852
- elsif operator == "prefix"
853
- capture_group = "(#{Regexp.escape(argument)}.*?)"
854
- elsif operator == "suffix"
855
- capture_group = "(.*?#{Regexp.escape(argument)})"
856
- end
857
- capture_group
858
- else
859
- capture_group = "(.*?)"
860
- if processor != nil && processor.respond_to?(:match)
861
- name = expansion[/\{([^\}=]+)(=[^\}]+)?\}/, 1]
862
- capture_group = "(#{processor.match(name)})"
863
1034
  end
864
- capture_group
865
- end
1035
+ end.join("#{joiner}?")
1036
+ "(?:|#{leader}#{combined})"
866
1037
  end
867
1038
 
868
1039
  # Ensure that the regular expression matches the whole URI.
869
1040
  regexp_string = "^#{regexp_string}$"
870
-
871
1041
  return expansions, Regexp.new(regexp_string)
872
1042
  end
873
1043
 
874
- ##
875
- # Extracts a URI Template opt operator.
876
- #
877
- # @param [String] value The unparsed value to extract from.
878
- # @param [#restore] processor The processor object.
879
- # @param [String] argument The argument to the operator.
880
- # @param [Array] variables The variables the operator is working on.
881
- # @param [Hash] mapping The mapping of variables to values.
882
- #
883
- # @return [String] The extracted result.
884
- def extract_opt_operator(
885
- value, processor, argument, variables, mapping)
886
- if value != "" && value != argument
887
- raise TemplateOperatorAbortedError,
888
- "Value for template operator 'opt' was unexpected."
889
- end
890
- end
891
-
892
- ##
893
- # Extracts a URI Template neg operator.
894
- #
895
- # @param [String] value The unparsed value to extract from.
896
- # @param [#restore] processor The processor object.
897
- # @param [String] argument The argument to the operator.
898
- # @param [Array] variables The variables the operator is working on.
899
- # @param [Hash] mapping The mapping of variables to values.
900
- #
901
- # @return [String] The extracted result.
902
- def extract_neg_operator(
903
- value, processor, argument, variables, mapping)
904
- if value != "" && value != argument
905
- raise TemplateOperatorAbortedError,
906
- "Value for template operator 'neg' was unexpected."
907
- end
908
- end
909
-
910
- ##
911
- # Extracts a URI Template prefix operator.
912
- #
913
- # @param [String] value The unparsed value to extract from.
914
- # @param [#restore] processor The processor object.
915
- # @param [String] argument The argument to the operator.
916
- # @param [Array] variables The variables the operator is working on.
917
- # @param [Hash] mapping The mapping of variables to values.
918
- #
919
- # @return [String] The extracted result.
920
- def extract_prefix_operator(
921
- value, processor, argument, variables, mapping)
922
- if variables.size != 1
923
- raise InvalidTemplateOperatorError,
924
- "Template operator 'prefix' takes exactly one variable."
925
- end
926
- if value[0...argument.size] != argument
927
- raise TemplateOperatorAbortedError,
928
- "Value for template operator 'prefix' missing expected prefix."
929
- end
930
- values = value.split(argument, -1)
931
- values << "" if value[-argument.size..-1] == argument
932
- values.shift if values[0] == ""
933
- values.pop if values[-1] == ""
934
-
935
- if processor && processor.respond_to?(:restore)
936
- values.map! { |value| processor.restore(variables.first, value) }
937
- end
938
- values = values.first if values.size == 1
939
- if mapping[variables.first] == nil || mapping[variables.first] == values
940
- mapping[variables.first] = values
941
- else
942
- raise TemplateOperatorAbortedError,
943
- "Value mismatch for repeated variable."
944
- end
945
- end
946
-
947
- ##
948
- # Extracts a URI Template suffix operator.
949
- #
950
- # @param [String] value The unparsed value to extract from.
951
- # @param [#restore] processor The processor object.
952
- # @param [String] argument The argument to the operator.
953
- # @param [Array] variables The variables the operator is working on.
954
- # @param [Hash] mapping The mapping of variables to values.
955
- #
956
- # @return [String] The extracted result.
957
- def extract_suffix_operator(
958
- value, processor, argument, variables, mapping)
959
- if variables.size != 1
960
- raise InvalidTemplateOperatorError,
961
- "Template operator 'suffix' takes exactly one variable."
962
- end
963
- if value[-argument.size..-1] != argument
964
- raise TemplateOperatorAbortedError,
965
- "Value for template operator 'suffix' missing expected suffix."
966
- end
967
- values = value.split(argument, -1)
968
- values.pop if values[-1] == ""
969
- if processor && processor.respond_to?(:restore)
970
- values.map! { |value| processor.restore(variables.first, value) }
971
- end
972
- values = values.first if values.size == 1
973
- if mapping[variables.first] == nil || mapping[variables.first] == values
974
- mapping[variables.first] = values
975
- else
976
- raise TemplateOperatorAbortedError,
977
- "Value mismatch for repeated variable."
978
- end
979
- end
980
-
981
- ##
982
- # Extracts a URI Template join operator.
983
- #
984
- # @param [String] value The unparsed value to extract from.
985
- # @param [#restore] processor The processor object.
986
- # @param [String] argument The argument to the operator.
987
- # @param [Array] variables The variables the operator is working on.
988
- # @param [Hash] mapping The mapping of variables to values.
989
- #
990
- # @return [String] The extracted result.
991
- def extract_join_operator(value, processor, argument, variables, mapping)
992
- unparsed_values = value.split(argument)
993
- parsed_variables = []
994
- for unparsed_value in unparsed_values
995
- name = unparsed_value[/^(.+?)=(.+)$/, 1]
996
- parsed_variables << name
997
- parsed_value = unparsed_value[/^(.+?)=(.+)$/, 2]
998
- if processor && processor.respond_to?(:restore)
999
- parsed_value = processor.restore(name, parsed_value)
1000
- end
1001
- if mapping[name] == nil || mapping[name] == parsed_value
1002
- mapping[name] = parsed_value
1003
- else
1004
- raise TemplateOperatorAbortedError,
1005
- "Value mismatch for repeated variable."
1006
- end
1007
- end
1008
- for variable in variables
1009
- if !parsed_variables.include?(variable) && mapping[variable] != nil
1010
- raise TemplateOperatorAbortedError,
1011
- "Value mismatch for repeated variable."
1012
- end
1013
- end
1014
- if (parsed_variables & variables) != parsed_variables
1015
- raise TemplateOperatorAbortedError,
1016
- "Template operator 'join' variable mismatch: " +
1017
- "#{parsed_variables.inspect}, #{variables.inspect}"
1018
- end
1019
- end
1020
-
1021
- ##
1022
- # Extracts a URI Template list operator.
1023
- #
1024
- # @param [String] value The unparsed value to extract from.
1025
- # @param [#restore] processor The processor object.
1026
- # @param [String] argument The argument to the operator.
1027
- # @param [Array] variables The variables the operator is working on.
1028
- # @param [Hash] mapping The mapping of variables to values.
1029
- #
1030
- # @return [String] The extracted result.
1031
- def extract_list_operator(value, processor, argument, variables, mapping)
1032
- if variables.size != 1
1033
- raise InvalidTemplateOperatorError,
1034
- "Template operator 'list' takes exactly one variable."
1035
- end
1036
- values = value.split(argument, -1)
1037
- values.pop if values[-1] == ""
1038
- if processor && processor.respond_to?(:restore)
1039
- values.map! { |value| processor.restore(variables.first, value) }
1040
- end
1041
- if mapping[variables.first] == nil || mapping[variables.first] == values
1042
- mapping[variables.first] = values
1043
- else
1044
- raise TemplateOperatorAbortedError,
1045
- "Value mismatch for repeated variable."
1046
- end
1047
- end
1048
1044
  end
1049
1045
  end