honkster-addressable 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1049 @@
1
+ # encoding:utf-8
2
+ #--
3
+ # Addressable, Copyright (c) 2006-2008 Bob Aman
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ #++
24
+
25
+ require "addressable/version"
26
+ require "addressable/uri"
27
+
28
+ module Addressable
29
+ ##
30
+ # This is an implementation of a URI template based on
31
+ # <a href="http://tinyurl.com/uritemplatedraft03">URI Template draft 03</a>.
32
+ class Template
33
+ # Constants used throughout the template code.
34
+ anything =
35
+ Addressable::URI::CharacterClasses::RESERVED +
36
+ Addressable::URI::CharacterClasses::UNRESERVED
37
+ OPERATOR_EXPANSION =
38
+ /\{-([a-zA-Z]+)\|([#{anything}]+)\|([#{anything}]+)\}/
39
+ VARIABLE_EXPANSION = /\{([#{anything}]+?)(?:=([#{anything}]+))?\}/
40
+
41
+ ##
42
+ # Raised if an invalid template value is supplied.
43
+ class InvalidTemplateValueError < StandardError
44
+ end
45
+
46
+ ##
47
+ # Raised if an invalid template operator is used in a pattern.
48
+ class InvalidTemplateOperatorError < StandardError
49
+ end
50
+
51
+ ##
52
+ # Raised if an invalid template operator is used in a pattern.
53
+ class TemplateOperatorAbortedError < StandardError
54
+ end
55
+
56
+ ##
57
+ # This class represents the data that is extracted when a Template
58
+ # is matched against a URI.
59
+ class MatchData
60
+ ##
61
+ # Creates a new MatchData object.
62
+ # MatchData objects should never be instantiated directly.
63
+ #
64
+ # @param [Addressable::URI] uri
65
+ # The URI that the template was matched against.
66
+ def initialize(uri, template, mapping) # :nodoc:
67
+ @uri = uri.dup.freeze
68
+ @template = template
69
+ @mapping = mapping.dup.freeze
70
+ end
71
+
72
+ ##
73
+ # @return [Addressable::URI]
74
+ # The URI that the Template was matched against.
75
+ attr_reader :uri
76
+
77
+ ##
78
+ # @return [Addressable::Template]
79
+ # The Template used for the match.
80
+ attr_reader :template
81
+
82
+ ##
83
+ # @return [Hash]
84
+ # The mapping that resulted from the match.
85
+ # Note that this mapping does not include keys or values for
86
+ # variables that appear in the Template, but are not present
87
+ # in the URI.
88
+ attr_reader :mapping
89
+
90
+ ##
91
+ # @return [Array]
92
+ # The list of variables that were present in the Template.
93
+ # Note that this list will include variables which do not appear
94
+ # in the mapping because they were not present in URI.
95
+ def variables
96
+ self.template.variables
97
+ end
98
+ alias_method :keys, :variables
99
+
100
+ ##
101
+ # @return [Array]
102
+ # The list of values that were captured by the Template.
103
+ # Note that this list will include nils for any variables which
104
+ # were in the Template, but did not appear in the URI.
105
+ def values
106
+ @values ||= self.variables.inject([]) do |accu, key|
107
+ accu << self.mapping[key]
108
+ accu
109
+ end
110
+ end
111
+ alias_method :captures, :values
112
+
113
+ ##
114
+ # Returns a <tt>String</tt> representation of the MatchData's state.
115
+ #
116
+ # @return [String] The MatchData's state, as a <tt>String</tt>.
117
+ def inspect
118
+ sprintf("#<%s:%#0x RESULT:%s>",
119
+ self.class.to_s, self.object_id, self.mapping.inspect)
120
+ end
121
+ end
122
+
123
+ ##
124
+ # Creates a new <tt>Addressable::Template</tt> object.
125
+ #
126
+ # @param [#to_str] pattern The URI Template pattern.
127
+ #
128
+ # @return [Addressable::Template] The initialized Template object.
129
+ def initialize(pattern)
130
+ if !pattern.respond_to?(:to_str)
131
+ raise TypeError, "Can't convert #{pattern.class} into String."
132
+ end
133
+ @pattern = pattern.to_str.freeze
134
+ end
135
+
136
+ ##
137
+ # @return [String] The Template object's pattern.
138
+ attr_reader :pattern
139
+
140
+ ##
141
+ # Returns a <tt>String</tt> representation of the Template object's state.
142
+ #
143
+ # @return [String] The Template object's state, as a <tt>String</tt>.
144
+ def inspect
145
+ sprintf("#<%s:%#0x PATTERN:%s>",
146
+ self.class.to_s, self.object_id, self.pattern)
147
+ end
148
+
149
+ ##
150
+ # Extracts a mapping from the URI using a URI Template pattern.
151
+ #
152
+ # @param [Addressable::URI, #to_str] uri
153
+ # The URI to extract from.
154
+ #
155
+ # @param [#restore, #match] processor
156
+ # A template processor object may optionally be supplied.
157
+ #
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.
167
+ #
168
+ # @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.
171
+ #
172
+ # @example
173
+ # class ExampleProcessor
174
+ # def self.restore(name, value)
175
+ # return value.gsub(/\+/, " ") if name == "query"
176
+ # return value
177
+ # end
178
+ #
179
+ # def self.match(name)
180
+ # return ".*?" if name == "first"
181
+ # return ".*"
182
+ # end
183
+ # end
184
+ #
185
+ # uri = Addressable::URI.parse(
186
+ # "http://example.com/search/an+example+search+query/"
187
+ # )
188
+ # Addressable::Template.new(
189
+ # "http://example.com/search/{query}/"
190
+ # ).extract(uri, ExampleProcessor)
191
+ # #=> {"query" => "an example search query"}
192
+ #
193
+ # uri = Addressable::URI.parse("http://example.com/a/b/c/")
194
+ # Addressable::Template.new(
195
+ # "http://example.com/{first}/{second}/"
196
+ # ).extract(uri, ExampleProcessor)
197
+ # #=> {"first" => "a", "second" => "b/c"}
198
+ #
199
+ # uri = Addressable::URI.parse("http://example.com/a/b/c/")
200
+ # Addressable::Template.new(
201
+ # "http://example.com/{first}/{-list|/|second}/"
202
+ # ).extract(uri)
203
+ # #=> {"first" => "a", "second" => ["b", "c"]}
204
+ def extract(uri, processor=nil)
205
+ match_data = self.match(uri, processor)
206
+ return (match_data ? match_data.mapping : nil)
207
+ end
208
+
209
+ ##
210
+ # Extracts match data from the URI using a URI Template pattern.
211
+ #
212
+ # @param [Addressable::URI, #to_str] uri
213
+ # The URI to extract from.
214
+ #
215
+ # @param [#restore, #match] processor
216
+ # A template processor object may optionally be supplied.
217
+ #
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.
227
+ #
228
+ # @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.
231
+ #
232
+ # @example
233
+ # class ExampleProcessor
234
+ # def self.restore(name, value)
235
+ # return value.gsub(/\+/, " ") if name == "query"
236
+ # return value
237
+ # end
238
+ #
239
+ # def self.match(name)
240
+ # return ".*?" if name == "first"
241
+ # return ".*"
242
+ # end
243
+ # end
244
+ #
245
+ # uri = Addressable::URI.parse(
246
+ # "http://example.com/search/an+example+search+query/"
247
+ # )
248
+ # match = Addressable::Template.new(
249
+ # "http://example.com/search/{query}/"
250
+ # ).match(uri, ExampleProcessor)
251
+ # match.variables
252
+ # #=> ["query"]
253
+ # match.captures
254
+ # #=> ["an example search query"]
255
+ #
256
+ # uri = Addressable::URI.parse("http://example.com/a/b/c/")
257
+ # match = Addressable::Template.new(
258
+ # "http://example.com/{first}/{second}/"
259
+ # ).match(uri, ExampleProcessor)
260
+ # match.variables
261
+ # #=> ["first", "second"]
262
+ # match.captures
263
+ # #=> ["a", "b/c"]
264
+ #
265
+ # uri = Addressable::URI.parse("http://example.com/a/b/c/")
266
+ # match = Addressable::Template.new(
267
+ # "http://example.com/{first}/{-list|/|second}/"
268
+ # ).match(uri)
269
+ # match.variables
270
+ # #=> ["first", "second"]
271
+ # match.captures
272
+ # #=> ["a", ["b", "c"]]
273
+ def match(uri, processor=nil)
274
+ uri = Addressable::URI.parse(uri)
275
+ mapping = {}
276
+
277
+ # First, we need to process the pattern, and extract the values.
278
+ expansions, expansion_regexp =
279
+ parse_template_pattern(pattern, processor)
280
+ unparsed_values = uri.to_str.scan(expansion_regexp).flatten
281
+
282
+ if uri.to_str == pattern
283
+ 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
303
+ end
304
+ end
305
+ else
306
+ name = expansion[VARIABLE_EXPANSION, 1]
307
+ value = unparsed_value
308
+ if processor != nil && processor.respond_to?(:restore)
309
+ value = processor.restore(name, value)
310
+ end
311
+ if mapping[name] == nil || mapping[name] == value
312
+ mapping[name] = value
313
+ else
314
+ return nil
315
+ end
316
+ end
317
+ end
318
+ return Addressable::Template::MatchData.new(uri, self, mapping)
319
+ else
320
+ return nil
321
+ end
322
+ end
323
+
324
+ ##
325
+ # Expands a URI template into another URI template.
326
+ #
327
+ # @param [Hash] mapping The mapping that corresponds to the pattern.
328
+ # @param [#validate, #transform] processor
329
+ # An optional processor object may be supplied.
330
+ #
331
+ # The object should respond to either the <tt>validate</tt> or
332
+ # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
333
+ # <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
334
+ # <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
335
+ # or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
336
+ # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
337
+ # exception will be raised if the value is invalid. The <tt>transform</tt>
338
+ # method should return the transformed variable value as a <tt>String</tt>.
339
+ # If a <tt>transform</tt> method is used, the value will not be percent
340
+ # encoded automatically. Unicode normalization will be performed both
341
+ # before and after sending the value to the transform method.
342
+ #
343
+ # @return [Addressable::Template] The partially expanded URI template.
344
+ #
345
+ # @example
346
+ # Addressable::Template.new(
347
+ # "http://example.com/{one}/{two}/"
348
+ # ).partial_expand({"one" => "1"}).pattern
349
+ # #=> "http://example.com/1/{two}/"
350
+ #
351
+ # 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}/"
360
+ # ).partial_expand({"one" => "1"}).pattern
361
+ # #=> "http://example.com/?one=1{-prefix|&two=|two}"
362
+ #
363
+ # Addressable::Template.new(
364
+ # "http://example.com/{-join|&|one,two,three}/"
365
+ # ).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)
368
+ 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
396
+ end
397
+ return Addressable::Template.new(result)
398
+ end
399
+
400
+ ##
401
+ # Expands a URI template into a full URI.
402
+ #
403
+ # @param [Hash] mapping The mapping that corresponds to the pattern.
404
+ # @param [#validate, #transform] processor
405
+ # An optional processor object may be supplied.
406
+ #
407
+ # The object should respond to either the <tt>validate</tt> or
408
+ # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
409
+ # <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
410
+ # <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
411
+ # or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
412
+ # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
413
+ # exception will be raised if the value is invalid. The <tt>transform</tt>
414
+ # method should return the transformed variable value as a <tt>String</tt>.
415
+ # If a <tt>transform</tt> method is used, the value will not be percent
416
+ # encoded automatically. Unicode normalization will be performed both
417
+ # before and after sending the value to the transform method.
418
+ #
419
+ # @return [Addressable::URI] The expanded URI template.
420
+ #
421
+ # @example
422
+ # class ExampleProcessor
423
+ # def self.validate(name, value)
424
+ # return !!(value =~ /^[\w ]+$/) if name == "query"
425
+ # return true
426
+ # end
427
+ #
428
+ # def self.transform(name, value)
429
+ # return value.gsub(/ /, "+") if name == "query"
430
+ # return value
431
+ # end
432
+ # end
433
+ #
434
+ # Addressable::Template.new(
435
+ # "http://example.com/search/{query}/"
436
+ # ).expand(
437
+ # {"query" => "an example search query"},
438
+ # ExampleProcessor
439
+ # ).to_str
440
+ # #=> "http://example.com/search/an+example+search+query/"
441
+ #
442
+ # Addressable::Template.new(
443
+ # "http://example.com/search/{-list|+|query}/"
444
+ # ).expand(
445
+ # {"query" => "an example search query".split(" ")}
446
+ # ).to_str
447
+ # #=> "http://example.com/search/an+example+search+query/"
448
+ #
449
+ # Addressable::Template.new(
450
+ # "http://example.com/search/{query}/"
451
+ # ).expand(
452
+ # {"query" => "bogus!"},
453
+ # ExampleProcessor
454
+ # ).to_str
455
+ # #=> Addressable::Template::InvalidTemplateValueError
456
+ def expand(mapping, processor=nil)
457
+ 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
476
+ end
477
+ return Addressable::URI.parse(result)
478
+ end
479
+
480
+ ##
481
+ # Returns an Array of variables used within the template pattern.
482
+ # The variables are listed in the Array in the order they appear within
483
+ # the pattern. Multiple occurrences of a variable within a pattern are
484
+ # not represented in this Array.
485
+ #
486
+ # @return [Array] The variables present in the template's pattern.
487
+ def variables
488
+ @variables ||= ordered_variable_defaults.map { |var, val| var }.uniq
489
+ end
490
+ alias_method :keys, :variables
491
+
492
+ ##
493
+ # Returns a mapping of variables to their default values specified
494
+ # in the template. Variables without defaults are not returned.
495
+ #
496
+ # @return [Hash] Mapping of template variables to their defaults
497
+ def variable_defaults
498
+ @variable_defaults ||=
499
+ Hash[*ordered_variable_defaults.reject { |k, v| v.nil? }.flatten]
500
+ end
501
+
502
+ private
503
+ 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]
514
+ end
515
+ result
516
+ end
517
+ end)
518
+ end
519
+
520
+ ##
521
+ # Transforms a mapping so that values can be substituted into the
522
+ # template.
523
+ #
524
+ # @param [Hash] mapping The mapping of variables to values.
525
+ # @param [#validate, #transform] processor
526
+ # An optional processor object may be supplied.
527
+ #
528
+ # The object should respond to either the <tt>validate</tt> or
529
+ # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
530
+ # <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
531
+ # <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
532
+ # or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
533
+ # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt> exception
534
+ # will be raised if the value is invalid. The <tt>transform</tt> method
535
+ # should return the transformed variable value as a <tt>String</tt>. If a
536
+ # <tt>transform</tt> method is used, the value will not be percent encoded
537
+ # automatically. Unicode normalization will be performed both before and
538
+ # after sending the value to the transform method.
539
+ #
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
545
+
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
550
+
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
602
+
603
+ accu[name] = transformed_value
604
+ accu
605
+ end
606
+ end
607
+
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
+ ""
627
+ end
628
+ end
629
+
630
+ ##
631
+ # Expands a URI Template neg operator.
632
+ #
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.
636
+ #
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
+ #
655
+ # @param [String] argument The argument to the operator.
656
+ # @param [Array] variables The variables the operator is working on.
657
+ # @param [Hash] mapping The mapping of variables to values.
658
+ #
659
+ # @return [String] The expanded result.
660
+ def expand_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
676
+
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
701
+
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])
716
+ 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]))
737
+ end
738
+ else
739
+ if !buffer.empty? && (buffer[-1..-1] != "}" || state == :prefix)
740
+ buffer << "{-opt|#{argument}|#{variable}}"
741
+ state = :prefix
742
+ end
743
+ if buffer.empty? && variables.size == 1
744
+ # Evaluates back to itself
745
+ buffer << "{-join|#{argument}|#{variable}}"
746
+ else
747
+ buffer << "{-prefix|#{variable}=|#{variable}}"
748
+ 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
755
+ end
756
+ end
757
+ else
758
+ raise InvalidTemplateOperatorError,
759
+ "Template operator 'join' does not accept Array values."
760
+ end
761
+ end
762
+ buffer
763
+ end
764
+ end
765
+
766
+ ##
767
+ # Expands a URI Template list operator.
768
+ #
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.
772
+ #
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."
778
+ 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
789
+ else
790
+ "{-list|#{argument}|#{variables.first}}"
791
+ end
792
+ end
793
+
794
+ ##
795
+ # Parses a URI template expansion <tt>String</tt>.
796
+ #
797
+ # @param [String] expansion The operator <tt>String</tt>.
798
+ # @param [Hash] mapping An optional mapping to merge defaults into.
799
+ #
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
809
+ accu
810
+ end).merge(mapping)
811
+ variables = variables.map { |var| var.gsub(/=.*$/, "") }
812
+ return operator, argument, variables, mapping
813
+ end
814
+
815
+ ##
816
+ # Generates the <tt>Regexp</tt> that parses a template pattern.
817
+ #
818
+ # @param [String] pattern The URI template pattern.
819
+ # @param [#match] processor The template processor to use.
820
+ #
821
+ # @return [Regexp]
822
+ # A regular expression which may be used to parse a template pattern.
823
+ def parse_template_pattern(pattern, processor=nil)
824
+ # Escape the pattern. The two gsubs restore the escaped curly braces
825
+ # back to their original form. Basically, escape everything that isn't
826
+ # within an expansion.
827
+ escaped_pattern = Regexp.escape(
828
+ pattern
829
+ ).gsub(/\\\{(.*?)\\\}/) do |escaped|
830
+ escaped.gsub(/\\(.)/, "\\1")
831
+ end
832
+
833
+ expansions = []
834
+
835
+ # Create a regular expression that captures the values of the
836
+ # variables in the URI.
837
+ regexp_string = escaped_pattern.gsub(
838
+ /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
839
+ ) do |expansion|
840
+ 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)})"
851
+ 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
+ end
864
+ capture_group
865
+ end
866
+ end
867
+
868
+ # Ensure that the regular expression matches the whole URI.
869
+ regexp_string = "^#{regexp_string}$"
870
+
871
+ return expansions, Regexp.new(regexp_string)
872
+ end
873
+
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
+ end
1049
+ end