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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +235 -0
- data/Gemfile +32 -0
- data/LICENSE.txt +202 -0
- data/README.md +121 -0
- data/Rakefile +9 -17
- data/data/unicode.data +0 -0
- data/lib/addressable.rb +4 -0
- data/lib/addressable/idna.rb +26 -4870
- data/lib/addressable/idna/native.rb +61 -0
- data/lib/addressable/idna/pure.rb +676 -0
- data/lib/addressable/template.rb +581 -585
- data/lib/addressable/uri.rb +870 -609
- data/lib/addressable/version.rb +16 -20
- data/spec/addressable/idna_spec.rb +170 -64
- data/spec/addressable/net_http_compat_spec.rb +30 -0
- data/spec/addressable/rack_mount_compat_spec.rb +106 -0
- data/spec/addressable/security_spec.rb +59 -0
- data/spec/addressable/template_spec.rb +1346 -2047
- data/spec/addressable/uri_spec.rb +3472 -1198
- data/spec/spec_helper.rb +24 -0
- data/tasks/clobber.rake +2 -0
- data/tasks/gem.rake +28 -19
- data/tasks/git.rake +9 -2
- data/tasks/metrics.rake +2 -0
- data/tasks/rspec.rake +23 -0
- data/tasks/yard.rake +7 -4
- metadata +78 -117
- data/CHANGELOG +0 -101
- data/LICENSE +0 -20
- data/README +0 -60
- data/tasks/rdoc.rake +0 -26
- data/tasks/rubyforge.rake +0 -89
- data/tasks/spec.rake +0 -47
- data/website/index.html +0 -110
data/lib/addressable/template.rb
CHANGED
@@ -1,42 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# encoding:utf-8
|
2
4
|
#--
|
3
|
-
#
|
5
|
+
# Copyright (C) Bob Aman
|
4
6
|
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
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
|
-
#
|
14
|
-
# included in all copies or substantial portions of the Software.
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
15
12
|
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
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
|
-
#
|
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
|
-
|
38
|
-
|
39
|
-
|
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)
|
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
|
-
#
|
159
|
-
#
|
160
|
-
# two parameters: [String] name and [String] value
|
161
|
-
# method should reverse any transformations that
|
162
|
-
# value to ensure a valid URI.
|
163
|
-
#
|
164
|
-
#
|
165
|
-
#
|
166
|
-
#
|
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
|
-
#
|
170
|
-
#
|
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
|
-
#
|
219
|
-
#
|
220
|
-
# two parameters: [String] name and [String] value
|
221
|
-
# method should reverse any transformations that
|
222
|
-
# value to ensure a valid URI.
|
223
|
-
#
|
224
|
-
#
|
225
|
-
#
|
226
|
-
#
|
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
|
-
#
|
230
|
-
#
|
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}/
|
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
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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
|
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/
|
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{
|
519
|
+
# #=> "http://example.com/?one=1{&two}/"
|
362
520
|
#
|
363
521
|
# Addressable::Template.new(
|
364
|
-
# "http://example.com/{
|
522
|
+
# "http://example.com/{?one,two,three}/"
|
365
523
|
# ).partial_expand({"one" => "1", "three" => 3}).pattern
|
366
|
-
# #=> "http://example.com/?one=1{
|
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
|
-
|
370
|
-
result.gsub!(
|
371
|
-
|
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/{
|
579
|
+
# "http://example.com/search/{query}/"
|
444
580
|
# ).expand(
|
445
|
-
# {"query" => "an example search query"
|
581
|
+
# {"query" => "an example search query"}
|
446
582
|
# ).to_str
|
447
|
-
# #=> "http://example.com/search/an
|
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
|
-
|
459
|
-
result.gsub!(
|
460
|
-
|
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 ||=
|
505
|
-
expansions,
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
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
|
-
|
516
|
-
|
517
|
-
end)
|
699
|
+
end.flatten
|
700
|
+
end
|
518
701
|
end
|
519
702
|
|
703
|
+
|
520
704
|
##
|
521
|
-
#
|
522
|
-
# template.
|
705
|
+
# Loops through each capture and expands any values available in mapping
|
523
706
|
#
|
524
|
-
# @param [Hash] mapping
|
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 [
|
541
|
-
def
|
542
|
-
|
543
|
-
|
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
|
-
|
547
|
-
raise TypeError,
|
548
|
-
"Can't convert #{value.class} into String or Array."
|
549
|
-
end
|
733
|
+
vars = varlist.split(",")
|
550
734
|
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
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
|
-
|
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
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
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
|
-
#
|
763
|
+
# Transforms a mapped value so that values can be substituted into the
|
764
|
+
# template.
|
632
765
|
#
|
633
|
-
# @param [
|
634
|
-
# @param [
|
635
|
-
#
|
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
|
-
#
|
656
|
-
#
|
657
|
-
#
|
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
|
660
|
-
def
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
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
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
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
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
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
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
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
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
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
|
-
|
744
|
-
|
745
|
-
|
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
|
-
|
855
|
+
transformed_value = Addressable::URI.encode_component(
|
856
|
+
value, encode_map)
|
748
857
|
end
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
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
|
-
|
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
|
-
#
|
926
|
+
# Takes a set of values, and joins them together based on the
|
927
|
+
# operator.
|
768
928
|
#
|
769
|
-
# @param [String]
|
770
|
-
#
|
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
|
774
|
-
def
|
775
|
-
|
776
|
-
|
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
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
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
|
-
|
948
|
+
value = Addressable::IDNA.unicode_normalize_kc(value)
|
791
949
|
end
|
950
|
+
value
|
792
951
|
end
|
793
952
|
|
794
953
|
##
|
795
|
-
#
|
954
|
+
# Generates a hash with string keys
|
796
955
|
#
|
797
|
-
# @param [
|
798
|
-
# @param [Hash] mapping An optional mapping to merge defaults into.
|
956
|
+
# @param [Hash] mapping A mapping hash to normalize
|
799
957
|
#
|
800
|
-
# @return [
|
801
|
-
# A
|
802
|
-
def
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
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
|
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
|
-
|
839
|
-
) do |expansion|
|
998
|
+
regexp_string = escaped_pattern.gsub( EXPRESSION ) do |expansion|
|
999
|
+
|
840
1000
|
expansions << expansion
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
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
|
-
|
865
|
-
|
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
|