fields-addressable 2.2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1049 @@
1
+ # encoding:utf-8
2
+ #--
3
+ # Addressable, Copyright (c) 2006-2010 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