mislav-addressable 2.1.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,1027 @@
1
+ # encoding:utf-8
2
+ #--
3
+ # Addressable, Copyright (c) 2006-2008 Bob Aman
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ #++
24
+
25
+ $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '/..')))
26
+ $:.uniq!
27
+
28
+ require "addressable/version"
29
+ require "addressable/uri"
30
+
31
+ module Addressable
32
+ ##
33
+ # This is an implementation of a URI template based on
34
+ # <a href="http://tinyurl.com/uritemplatedraft03">URI Template draft 03</a>.
35
+ class Template
36
+ # Constants used throughout the template code.
37
+ anything =
38
+ Addressable::URI::CharacterClasses::RESERVED +
39
+ Addressable::URI::CharacterClasses::UNRESERVED
40
+ OPERATOR_EXPANSION =
41
+ /\{-([a-zA-Z]+)\|([#{anything}]+)\|([#{anything}]+)\}/
42
+ VARIABLE_EXPANSION = /\{([#{anything}]+?)(=([#{anything}]+))?\}/
43
+
44
+ ##
45
+ # Raised if an invalid template value is supplied.
46
+ class InvalidTemplateValueError < StandardError
47
+ end
48
+
49
+ ##
50
+ # Raised if an invalid template operator is used in a pattern.
51
+ class InvalidTemplateOperatorError < StandardError
52
+ end
53
+
54
+ ##
55
+ # Raised if an invalid template operator is used in a pattern.
56
+ class TemplateOperatorAbortedError < StandardError
57
+ end
58
+
59
+ ##
60
+ # This class represents the data that is extracted when a Template
61
+ # is matched against a URI.
62
+ class MatchData
63
+ ##
64
+ # Creates a new MatchData object.
65
+ # MatchData objects should never be instantiated directly.
66
+ #
67
+ # @param [Addressable::URI] uri
68
+ # The URI that the template was matched against.
69
+ def initialize(uri, template, mapping) # :nodoc:
70
+ @uri = uri.dup.freeze
71
+ @template = template
72
+ @mapping = mapping.dup.freeze
73
+ end
74
+
75
+ ##
76
+ # @return [Addressable::URI]
77
+ # The URI that the Template was matched against.
78
+ attr_reader :uri
79
+
80
+ ##
81
+ # @return [Addressable::Template]
82
+ # The Template used for the match.
83
+ attr_reader :template
84
+
85
+ ##
86
+ # @return [Hash]
87
+ # The mapping that resulted from the match.
88
+ # Note that this mapping does not include keys or values for
89
+ # variables that appear in the Template, but are not present
90
+ # in the URI.
91
+ attr_reader :mapping
92
+
93
+ ##
94
+ # @return [Array]
95
+ # The list of variables that were present in the Template.
96
+ # Note that this list will include variables which do not appear
97
+ # in the mapping because they were not present in URI.
98
+ def variables
99
+ self.template.variables
100
+ end
101
+ alias_method :keys, :variables
102
+
103
+ ##
104
+ # @return [Array]
105
+ # The list of values that were captured by the Template.
106
+ # Note that this list will include nils for any variables which
107
+ # were in the Template, but did not appear in the URI.
108
+ def values
109
+ @values ||= self.variables.inject([]) do |accu, key|
110
+ accu << self.mapping[key]
111
+ accu
112
+ end
113
+ end
114
+ alias_method :captures, :values
115
+
116
+ ##
117
+ # Returns a <tt>String</tt> representation of the MatchData's state.
118
+ #
119
+ # @return [String] The MatchData's state, as a <tt>String</tt>.
120
+ def inspect
121
+ sprintf("#<%s:%#0x RESULT:%s>",
122
+ self.class.to_s, self.object_id, self.mapping.inspect)
123
+ end
124
+ end
125
+
126
+ ##
127
+ # Creates a new <tt>Addressable::Template</tt> object.
128
+ #
129
+ # @param [#to_str] pattern The URI Template pattern.
130
+ #
131
+ # @return [Addressable::Template] The initialized Template object.
132
+ def initialize(pattern)
133
+ if !pattern.respond_to?(:to_str)
134
+ raise TypeError, "Can't convert #{pattern.class} into String."
135
+ end
136
+ @pattern = pattern.to_str.freeze
137
+ end
138
+
139
+ ##
140
+ # @return [String] The Template object's pattern.
141
+ attr_reader :pattern
142
+
143
+ ##
144
+ # Returns a <tt>String</tt> representation of the Template object's state.
145
+ #
146
+ # @return [String] The Template object's state, as a <tt>String</tt>.
147
+ def inspect
148
+ sprintf("#<%s:%#0x PATTERN:%s>",
149
+ self.class.to_s, self.object_id, self.pattern)
150
+ end
151
+
152
+ ##
153
+ # Extracts a mapping from the URI using a URI Template pattern.
154
+ #
155
+ # @param [Addressable::URI, #to_str] uri
156
+ # The URI to extract from.
157
+ # @param [#restore, #match] processor
158
+ # A template processor object may optionally be supplied.
159
+ # The object should respond to either the <tt>restore</tt> or
160
+ # <tt>match</tt> messages or both. The <tt>restore</tt> method should
161
+ # take two parameters: [String] name and [String] value. The
162
+ # <tt>restore</tt> method should reverse any transformations that have
163
+ # been performed on the value to ensure a valid URI. The
164
+ # <tt>match</tt> method should take a single parameter: [String] name.
165
+ # The <tt>match</tt> method should return a <tt>String</tt> containing
166
+ # a regular expression capture group for matching on that particular
167
+ # variable. The default value is ".*?". The <tt>match</tt> method has
168
+ # no effect on multivariate operator expansions.
169
+ # @return [Hash, NilClass]
170
+ # The <tt>Hash</tt> mapping that was extracted from the URI, or
171
+ # <tt>nil</tt> if the URI didn't match the template.
172
+ #
173
+ # @example
174
+ # class ExampleProcessor
175
+ # def self.restore(name, value)
176
+ # return value.gsub(/\+/, " ") if name == "query"
177
+ # return value
178
+ # end
179
+ #
180
+ # def self.match(name)
181
+ # return ".*?" if name == "first"
182
+ # return ".*"
183
+ # end
184
+ # end
185
+ #
186
+ # uri = Addressable::URI.parse(
187
+ # "http://example.com/search/an+example+search+query/"
188
+ # )
189
+ # Addressable::Template.new(
190
+ # "http://example.com/search/{query}/"
191
+ # ).extract(uri, ExampleProcessor)
192
+ # #=> {"query" => "an example search query"}
193
+ #
194
+ # uri = Addressable::URI.parse("http://example.com/a/b/c/")
195
+ # Addressable::Template.new(
196
+ # "http://example.com/{first}/{second}/"
197
+ # ).extract(uri, ExampleProcessor)
198
+ # #=> {"first" => "a", "second" => "b/c"}
199
+ #
200
+ # uri = Addressable::URI.parse("http://example.com/a/b/c/")
201
+ # Addressable::Template.new(
202
+ # "http://example.com/{first}/{-list|/|second}/"
203
+ # ).extract(uri)
204
+ # #=> {"first" => "a", "second" => ["b", "c"]}
205
+ def extract(uri, processor=nil)
206
+ match_data = self.match(uri, processor)
207
+ return (match_data ? match_data.mapping : nil)
208
+ end
209
+
210
+ ##
211
+ # Extracts match data from the URI using a URI Template pattern.
212
+ #
213
+ # @param [Addressable::URI, #to_str] uri
214
+ # The URI to extract from.
215
+ # @param [#restore, #match] processor
216
+ # A template processor object may optionally be supplied.
217
+ # The object should respond to either the <tt>restore</tt> or
218
+ # <tt>match</tt> messages or both. The <tt>restore</tt> method should
219
+ # take two parameters: [String] name and [String] value. The
220
+ # <tt>restore</tt> method should reverse any transformations that have
221
+ # been performed on the value to ensure a valid URI. The
222
+ # <tt>match</tt> method should take a single parameter: [String] name.
223
+ # The <tt>match</tt> method should return a <tt>String</tt> containing
224
+ # a regular expression capture group for matching on that particular
225
+ # variable. The default value is ".*?". The <tt>match</tt> method has
226
+ # no effect on multivariate operator expansions.
227
+ # @return [Hash, NilClass]
228
+ # The <tt>Hash</tt> mapping that was extracted from the URI, or
229
+ # <tt>nil</tt> if the URI didn't match the template.
230
+ #
231
+ # @example
232
+ # class ExampleProcessor
233
+ # def self.restore(name, value)
234
+ # return value.gsub(/\+/, " ") if name == "query"
235
+ # return value
236
+ # end
237
+ #
238
+ # def self.match(name)
239
+ # return ".*?" if name == "first"
240
+ # return ".*"
241
+ # end
242
+ # end
243
+ #
244
+ # uri = Addressable::URI.parse(
245
+ # "http://example.com/search/an+example+search+query/"
246
+ # )
247
+ # match = Addressable::Template.new(
248
+ # "http://example.com/search/{query}/"
249
+ # ).match(uri, ExampleProcessor)
250
+ # match.variables
251
+ # #=> ["query"]
252
+ # match.captures
253
+ # #=> ["an example search query"]
254
+ #
255
+ # uri = Addressable::URI.parse("http://example.com/a/b/c/")
256
+ # match = Addressable::Template.new(
257
+ # "http://example.com/{first}/{second}/"
258
+ # ).match(uri, ExampleProcessor)
259
+ # match.variables
260
+ # #=> ["first", "second"]
261
+ # match.captures
262
+ # #=> ["a", "b/c"]
263
+ #
264
+ # uri = Addressable::URI.parse("http://example.com/a/b/c/")
265
+ # match = Addressable::Template.new(
266
+ # "http://example.com/{first}/{-list|/|second}/"
267
+ # ).match(uri)
268
+ # match.variables
269
+ # #=> ["first", "second"]
270
+ # match.captures
271
+ # #=> ["a", ["b", "c"]]
272
+ def match(uri, processor=nil)
273
+ uri = Addressable::URI.parse(uri)
274
+ mapping = {}
275
+
276
+ # First, we need to process the pattern, and extract the values.
277
+ expansions, expansion_regexp =
278
+ parse_template_pattern(pattern, processor)
279
+ unparsed_values = uri.to_str.scan(expansion_regexp).flatten
280
+
281
+ if uri.to_str == pattern
282
+ return Addressable::Template::MatchData.new(uri, self, mapping)
283
+ elsif expansions.size > 0 && expansions.size == unparsed_values.size
284
+ expansions.each_with_index do |expansion, index|
285
+ unparsed_value = unparsed_values[index]
286
+ if expansion =~ OPERATOR_EXPANSION
287
+ operator, argument, variables =
288
+ parse_template_expansion(expansion)
289
+ extract_method = "extract_#{operator}_operator"
290
+ if ([extract_method, extract_method.to_sym] &
291
+ private_methods).empty?
292
+ raise InvalidTemplateOperatorError,
293
+ "Invalid template operator: #{operator}"
294
+ else
295
+ begin
296
+ send(
297
+ extract_method.to_sym, unparsed_value, processor,
298
+ argument, variables, mapping
299
+ )
300
+ rescue TemplateOperatorAbortedError
301
+ return nil
302
+ end
303
+ end
304
+ else
305
+ name = expansion[VARIABLE_EXPANSION, 1]
306
+ value = unparsed_value
307
+ if processor != nil && processor.respond_to?(:restore)
308
+ value = processor.restore(name, value)
309
+ end
310
+ if mapping[name] == nil || mapping[name] == value
311
+ mapping[name] = value
312
+ else
313
+ return nil
314
+ end
315
+ end
316
+ end
317
+ return Addressable::Template::MatchData.new(uri, self, mapping)
318
+ else
319
+ return nil
320
+ end
321
+ end
322
+
323
+ ##
324
+ # Expands a URI template into another URI template.
325
+ #
326
+ # @param [Hash] mapping The mapping that corresponds to the pattern.
327
+ # @param [#validate, #transform] processor
328
+ # An optional processor object may be supplied. The object should
329
+ # respond to either the <tt>validate</tt> or <tt>transform</tt> messages
330
+ # or both. Both the <tt>validate</tt> and <tt>transform</tt> methods
331
+ # should take two parameters: <tt>name</tt> and <tt>value</tt>. The
332
+ # <tt>validate</tt> method should return <tt>true</tt> or
333
+ # <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
334
+ # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
335
+ # exception will be raised if the value is invalid. The
336
+ # <tt>transform</tt> method should return the transformed variable
337
+ # value as a <tt>String</tt>. If a <tt>transform</tt> method is used,
338
+ # the value will not be percent encoded automatically. Unicode
339
+ # normalization will be performed both before and after sending the
340
+ # value to the transform method.
341
+ #
342
+ # @return [Addressable::Template] The partially expanded URI template.
343
+ #
344
+ # @example
345
+ # Addressable::Template.new(
346
+ # "http://example.com/{one}/{two}/"
347
+ # ).partial_expand({"one" => "1"}).pattern
348
+ # #=> "http://example.com/1/{two}/"
349
+ #
350
+ # Addressable::Template.new(
351
+ # "http://example.com/search/{-list|+|query}/"
352
+ # ).partial_expand(
353
+ # {"query" => "an example search query".split(" ")}
354
+ # ).pattern
355
+ # #=> "http://example.com/search/an+example+search+query/"
356
+ #
357
+ # Addressable::Template.new(
358
+ # "http://example.com/{-join|&|one,two}/"
359
+ # ).partial_expand({"one" => "1"}).pattern
360
+ # #=> "http://example.com/?one=1{-prefix|&two=|two}"
361
+ #
362
+ # Addressable::Template.new(
363
+ # "http://example.com/{-join|&|one,two,three}/"
364
+ # ).partial_expand({"one" => "1", "three" => 3}).pattern
365
+ # #=> "http://example.com/?one=1{-prefix|&two=|two}&three=3"
366
+ def partial_expand(mapping, processor=nil)
367
+ result = self.pattern.dup
368
+ transformed_mapping = transform_mapping(mapping, processor)
369
+ result.gsub!(
370
+ /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
371
+ ) do |capture|
372
+ if capture =~ OPERATOR_EXPANSION
373
+ operator, argument, variables, default_mapping =
374
+ parse_template_expansion(capture, transformed_mapping)
375
+ expand_method = "expand_#{operator}_operator"
376
+ if ([expand_method, expand_method.to_sym] & private_methods).empty?
377
+ raise InvalidTemplateOperatorError,
378
+ "Invalid template operator: #{operator}"
379
+ else
380
+ send(
381
+ expand_method.to_sym, argument, variables,
382
+ default_mapping, true
383
+ )
384
+ end
385
+ else
386
+ varname, _, vardefault = capture.scan(/^\{(.+?)(=(.*))?\}$/)[0]
387
+ if transformed_mapping[varname]
388
+ transformed_mapping[varname]
389
+ elsif vardefault
390
+ "{#{varname}=#{vardefault}}"
391
+ else
392
+ "{#{varname}}"
393
+ end
394
+ end
395
+ end
396
+ return Addressable::Template.new(result)
397
+ end
398
+
399
+ ##
400
+ # Expands a URI template into a full URI.
401
+ #
402
+ # @param [Hash] mapping The mapping that corresponds to the pattern.
403
+ # @param [#validate, #transform] processor
404
+ # An optional processor object may be supplied. The object should
405
+ # respond to either the <tt>validate</tt> or <tt>transform</tt> messages
406
+ # or both. Both the <tt>validate</tt> and <tt>transform</tt> methods
407
+ # should take two parameters: <tt>name</tt> and <tt>value</tt>. The
408
+ # <tt>validate</tt> method should return <tt>true</tt> or
409
+ # <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
410
+ # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
411
+ # exception will be raised if the value is invalid. The
412
+ # <tt>transform</tt> method should return the transformed variable
413
+ # value as a <tt>String</tt>. If a <tt>transform</tt> method is used,
414
+ # the value will not be percent encoded automatically. Unicode
415
+ # normalization will be performed both before and after sending the
416
+ # value to the transform method.
417
+ #
418
+ # @return [Addressable::URI] The expanded URI template.
419
+ #
420
+ # @example
421
+ # class ExampleProcessor
422
+ # def self.validate(name, value)
423
+ # return !!(value =~ /^[\w ]+$/) if name == "query"
424
+ # return true
425
+ # end
426
+ #
427
+ # def self.transform(name, value)
428
+ # return value.gsub(/ /, "+") if name == "query"
429
+ # return value
430
+ # end
431
+ # end
432
+ #
433
+ # Addressable::Template.new(
434
+ # "http://example.com/search/{query}/"
435
+ # ).expand(
436
+ # {"query" => "an example search query"},
437
+ # ExampleProcessor
438
+ # ).to_str
439
+ # #=> "http://example.com/search/an+example+search+query/"
440
+ #
441
+ # Addressable::Template.new(
442
+ # "http://example.com/search/{-list|+|query}/"
443
+ # ).expand(
444
+ # {"query" => "an example search query".split(" ")}
445
+ # ).to_str
446
+ # #=> "http://example.com/search/an+example+search+query/"
447
+ #
448
+ # Addressable::Template.new(
449
+ # "http://example.com/search/{query}/"
450
+ # ).expand(
451
+ # {"query" => "bogus!"},
452
+ # ExampleProcessor
453
+ # ).to_str
454
+ # #=> Addressable::Template::InvalidTemplateValueError
455
+ def expand(mapping, processor=nil)
456
+ result = self.pattern.dup
457
+ transformed_mapping = transform_mapping(mapping, processor)
458
+ result.gsub!(
459
+ /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
460
+ ) do |capture|
461
+ if capture =~ OPERATOR_EXPANSION
462
+ operator, argument, variables, default_mapping =
463
+ parse_template_expansion(capture, transformed_mapping)
464
+ expand_method = "expand_#{operator}_operator"
465
+ if ([expand_method, expand_method.to_sym] & private_methods).empty?
466
+ raise InvalidTemplateOperatorError,
467
+ "Invalid template operator: #{operator}"
468
+ else
469
+ send(expand_method.to_sym, argument, variables, default_mapping)
470
+ end
471
+ else
472
+ varname, _, vardefault = capture.scan(/^\{(.+?)(=(.*))?\}$/)[0]
473
+ transformed_mapping[varname] || vardefault
474
+ end
475
+ end
476
+ return Addressable::URI.parse(result)
477
+ end
478
+
479
+ ##
480
+ # Returns an Array of variables used within the template pattern.
481
+ # The variables are listed in the Array in the order they appear within
482
+ # the pattern. Multiple occurrences of a variable within a pattern are
483
+ # not represented in this Array.
484
+ #
485
+ # @return [Array] The variables present in the template's pattern.
486
+ def variables
487
+ @variables ||= (begin
488
+ result = []
489
+
490
+ expansions, expansion_regexp = parse_template_pattern(pattern)
491
+ expansions.each do |expansion|
492
+ if expansion =~ OPERATOR_EXPANSION
493
+ _, _, variables, _ = parse_template_expansion(expansion)
494
+ result.concat(variables)
495
+ else
496
+ result << expansion[VARIABLE_EXPANSION, 1]
497
+ end
498
+ end
499
+ result.uniq
500
+ end)
501
+ end
502
+ alias_method :keys, :variables
503
+
504
+ private
505
+ ##
506
+ # Transforms a mapping so that values can be substituted into the
507
+ # template.
508
+ #
509
+ # @param [Hash] mapping The mapping of variables to values.
510
+ # @param [#validate, #transform] processor
511
+ # An optional processor object may be supplied. The object should
512
+ # respond to either the <tt>validate</tt> or <tt>transform</tt> messages
513
+ # or both. Both the <tt>validate</tt> and <tt>transform</tt> methods
514
+ # should take two parameters: <tt>name</tt> and <tt>value</tt>. The
515
+ # <tt>validate</tt> method should return <tt>true</tt> or
516
+ # <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
517
+ # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
518
+ # exception will be raised if the value is invalid. The
519
+ # <tt>transform</tt> method should return the transformed variable
520
+ # value as a <tt>String</tt>. If a <tt>transform</tt> method is used,
521
+ # the value will not be percent encoded automatically. Unicode
522
+ # normalization will be performed both before and after sending the
523
+ # value to the transform method.
524
+ #
525
+ # @return [Hash] The transformed mapping.
526
+ def transform_mapping(mapping, processor=nil)
527
+ return mapping.inject({}) do |accu, pair|
528
+ name, value = pair
529
+ value = value.to_s if Numeric === value
530
+
531
+ unless value.respond_to?(:to_ary) || value.respond_to?(:to_str)
532
+ raise TypeError,
533
+ "Can't convert #{value.class} into String or Array."
534
+ end
535
+
536
+ name = name.to_s
537
+ value = value.respond_to?(:to_ary) ? value.to_ary : value.to_str
538
+
539
+ # Handle unicode normalization
540
+ if value.kind_of?(Array)
541
+ value.map! { |val| Addressable::IDNA.unicode_normalize_kc(val) }
542
+ else
543
+ value = Addressable::IDNA.unicode_normalize_kc(value)
544
+ end
545
+
546
+ if processor == nil || !processor.respond_to?(:transform)
547
+ # Handle percent escaping
548
+ if value.kind_of?(Array)
549
+ transformed_value = value.map do |val|
550
+ Addressable::URI.encode_component(
551
+ val, Addressable::URI::CharacterClasses::UNRESERVED)
552
+ end
553
+ else
554
+ transformed_value = Addressable::URI.encode_component(
555
+ value, Addressable::URI::CharacterClasses::UNRESERVED)
556
+ end
557
+ end
558
+
559
+ # Process, if we've got a processor
560
+ if processor != nil
561
+ if processor.respond_to?(:validate)
562
+ if !processor.validate(name, value)
563
+ display_value = value.kind_of?(Array) ? value.inspect : value
564
+ raise InvalidTemplateValueError,
565
+ "#{name}=#{display_value} is an invalid template value."
566
+ end
567
+ end
568
+ if processor.respond_to?(:transform)
569
+ transformed_value = processor.transform(name, value)
570
+ if transformed_value.kind_of?(Array)
571
+ transformed_value.map! do |val|
572
+ Addressable::IDNA.unicode_normalize_kc(val)
573
+ end
574
+ else
575
+ transformed_value =
576
+ Addressable::IDNA.unicode_normalize_kc(transformed_value)
577
+ end
578
+ end
579
+ end
580
+
581
+ accu[name] = transformed_value
582
+ accu
583
+ end
584
+ end
585
+
586
+ ##
587
+ # Expands a URI Template opt operator.
588
+ #
589
+ # @param [String] argument The argument to the operator.
590
+ # @param [Array] variables The variables the operator is working on.
591
+ # @param [Hash] mapping The mapping of variables to values.
592
+ #
593
+ # @return [String] The expanded result.
594
+ def expand_opt_operator(argument, variables, mapping, partial=false)
595
+ variables_present = variables.any? do |variable|
596
+ mapping[variable] != [] &&
597
+ mapping[variable]
598
+ end
599
+ if partial && !variables_present
600
+ "{-opt|#{argument}|#{variables.join(",")}}"
601
+ elsif variables_present
602
+ argument
603
+ else
604
+ ""
605
+ end
606
+ end
607
+
608
+ ##
609
+ # Expands a URI Template neg 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_neg_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
+ "{-neg|#{argument}|#{variables.join(",")}}"
623
+ elsif variables_present
624
+ ""
625
+ else
626
+ argument
627
+ end
628
+ end
629
+
630
+ ##
631
+ # Expands a URI Template prefix 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_prefix_operator(argument, variables, mapping, partial=false)
639
+ if variables.size != 1
640
+ raise InvalidTemplateOperatorError,
641
+ "Template operator 'prefix' takes exactly one variable."
642
+ end
643
+ value = mapping[variables.first]
644
+ if !partial || value
645
+ if value.kind_of?(Array)
646
+ (value.map { |list_value| argument + list_value }).join("")
647
+ elsif value
648
+ argument + value.to_s
649
+ end
650
+ else
651
+ "{-prefix|#{argument}|#{variables.first}}"
652
+ end
653
+ end
654
+
655
+ ##
656
+ # Expands a URI Template suffix operator.
657
+ #
658
+ # @param [String] argument The argument to the operator.
659
+ # @param [Array] variables The variables the operator is working on.
660
+ # @param [Hash] mapping The mapping of variables to values.
661
+ #
662
+ # @return [String] The expanded result.
663
+ def expand_suffix_operator(argument, variables, mapping, partial=false)
664
+ if variables.size != 1
665
+ raise InvalidTemplateOperatorError,
666
+ "Template operator 'suffix' takes exactly one variable."
667
+ end
668
+ value = mapping[variables.first]
669
+ if !partial || value
670
+ if value.kind_of?(Array)
671
+ (value.map { |list_value| list_value + argument }).join("")
672
+ elsif value
673
+ value.to_s + argument
674
+ end
675
+ else
676
+ "{-suffix|#{argument}|#{variables.first}}"
677
+ end
678
+ end
679
+
680
+ ##
681
+ # Expands a URI Template join operator.
682
+ #
683
+ # @param [String] argument The argument to the operator.
684
+ # @param [Array] variables The variables the operator is working on.
685
+ # @param [Hash] mapping The mapping of variables to values.
686
+ #
687
+ # @return [String] The expanded result.
688
+ def expand_join_operator(argument, variables, mapping, partial=false)
689
+ if !partial
690
+ variable_values = variables.inject([]) do |accu, variable|
691
+ if !mapping[variable].kind_of?(Array)
692
+ if mapping[variable]
693
+ accu << variable + "=" + (mapping[variable])
694
+ end
695
+ else
696
+ raise InvalidTemplateOperatorError,
697
+ "Template operator 'join' does not accept Array values."
698
+ end
699
+ accu
700
+ end
701
+ variable_values.join(argument)
702
+ else
703
+ buffer = ""
704
+ state = :suffix
705
+ variables.each_with_index do |variable, index|
706
+ if !mapping[variable].kind_of?(Array)
707
+ if mapping[variable]
708
+ if buffer.empty? || buffer[-1..-1] == "}"
709
+ buffer << (variable + "=" + (mapping[variable]))
710
+ elsif state == :suffix
711
+ buffer << argument
712
+ buffer << (variable + "=" + (mapping[variable]))
713
+ else
714
+ buffer << (variable + "=" + (mapping[variable]))
715
+ end
716
+ else
717
+ if !buffer.empty? && (buffer[-1..-1] != "}" || state == :prefix)
718
+ buffer << "{-opt|#{argument}|#{variable}}"
719
+ state = :prefix
720
+ end
721
+ if buffer.empty? && variables.size == 1
722
+ # Evaluates back to itself
723
+ buffer << "{-join|#{argument}|#{variable}}"
724
+ else
725
+ buffer << "{-prefix|#{variable}=|#{variable}}"
726
+ end
727
+ if (index != (variables.size - 1) && state == :suffix)
728
+ buffer << "{-opt|#{argument}|#{variable}}"
729
+ elsif index != (variables.size - 1) &&
730
+ mapping[variables[index + 1]]
731
+ buffer << argument
732
+ state = :prefix
733
+ end
734
+ end
735
+ else
736
+ raise InvalidTemplateOperatorError,
737
+ "Template operator 'join' does not accept Array values."
738
+ end
739
+ end
740
+ buffer
741
+ end
742
+ end
743
+
744
+ ##
745
+ # Expands a URI Template list operator.
746
+ #
747
+ # @param [String] argument The argument to the operator.
748
+ # @param [Array] variables The variables the operator is working on.
749
+ # @param [Hash] mapping The mapping of variables to values.
750
+ #
751
+ # @return [String] The expanded result.
752
+ def expand_list_operator(argument, variables, mapping, partial=false)
753
+ if variables.size != 1
754
+ raise InvalidTemplateOperatorError,
755
+ "Template operator 'list' takes exactly one variable."
756
+ end
757
+ if !partial || mapping[variables.first]
758
+ values = mapping[variables.first]
759
+ if values
760
+ if values.kind_of?(Array)
761
+ values.join(argument)
762
+ else
763
+ raise InvalidTemplateOperatorError,
764
+ "Template operator 'list' only accepts Array values."
765
+ end
766
+ end
767
+ else
768
+ "{-list|#{argument}|#{variables.first}}"
769
+ end
770
+ end
771
+
772
+ ##
773
+ # Parses a URI template expansion <tt>String</tt>.
774
+ #
775
+ # @param [String] expansion The operator <tt>String</tt>.
776
+ # @param [Hash] mapping An optional mapping to merge defaults into.
777
+ #
778
+ # @return [Array]
779
+ # A tuple of the operator, argument, variables, and mapping.
780
+ def parse_template_expansion(capture, mapping={})
781
+ operator, argument, variables = capture[1...-1].split("|")
782
+ operator.gsub!(/^\-/, "")
783
+ variables = variables.split(",")
784
+ mapping = (variables.inject({}) do |accu, var|
785
+ varname, _, vardefault = var.scan(/^(.+?)(=(.*))?$/)[0]
786
+ accu[varname] = vardefault
787
+ accu
788
+ end).merge(mapping)
789
+ variables = variables.map { |var| var.gsub(/=.*$/, "") }
790
+ return operator, argument, variables, mapping
791
+ end
792
+
793
+ ##
794
+ # Generates the <tt>Regexp</tt> that parses a template pattern.
795
+ #
796
+ # @param [String] pattern The URI template pattern.
797
+ # @param [#match] processor The template processor to use.
798
+ #
799
+ # @return [Regexp]
800
+ # A regular expression which may be used to parse a template pattern.
801
+ def parse_template_pattern(pattern, processor=nil)
802
+ # Escape the pattern. The two gsubs restore the escaped curly braces
803
+ # back to their original form. Basically, escape everything that isn't
804
+ # within an expansion.
805
+ escaped_pattern = Regexp.escape(
806
+ pattern
807
+ ).gsub(/\\\{(.*?)\\\}/) do |escaped|
808
+ escaped.gsub(/\\(.)/, "\\1")
809
+ end
810
+
811
+ expansions = []
812
+
813
+ # Create a regular expression that captures the values of the
814
+ # variables in the URI.
815
+ regexp_string = escaped_pattern.gsub(
816
+ /#{OPERATOR_EXPANSION}|#{VARIABLE_EXPANSION}/
817
+ ) do |expansion|
818
+ expansions << expansion
819
+ if expansion =~ OPERATOR_EXPANSION
820
+ capture_group = "(.*)"
821
+ operator, argument, names, _ =
822
+ parse_template_expansion(expansion)
823
+ if processor != nil && processor.respond_to?(:match)
824
+ # We can only lookup the match values for single variable
825
+ # operator expansions. Besides, ".*" is usually the only
826
+ # reasonable value for multivariate operators anyways.
827
+ if ["prefix", "suffix", "list"].include?(operator)
828
+ capture_group = "(#{processor.match(names.first)})"
829
+ end
830
+ elsif operator == "prefix"
831
+ capture_group = "(#{Regexp.escape(argument)}.*?)"
832
+ elsif operator == "suffix"
833
+ capture_group = "(.*?#{Regexp.escape(argument)})"
834
+ end
835
+ capture_group
836
+ else
837
+ capture_group = "(.*?)"
838
+ if processor != nil && processor.respond_to?(:match)
839
+ name = expansion[/\{([^\}=]+)(=[^\}]+)?\}/, 1]
840
+ capture_group = "(#{processor.match(name)})"
841
+ end
842
+ capture_group
843
+ end
844
+ end
845
+
846
+ # Ensure that the regular expression matches the whole URI.
847
+ regexp_string = "^#{regexp_string}$"
848
+
849
+ return expansions, Regexp.new(regexp_string)
850
+ end
851
+
852
+ ##
853
+ # Extracts a URI Template opt operator.
854
+ #
855
+ # @param [String] value The unparsed value to extract from.
856
+ # @param [#restore] processor The processor object.
857
+ # @param [String] argument The argument to the operator.
858
+ # @param [Array] variables The variables the operator is working on.
859
+ # @param [Hash] mapping The mapping of variables to values.
860
+ #
861
+ # @return [String] The extracted result.
862
+ def extract_opt_operator(
863
+ value, processor, argument, variables, mapping)
864
+ if value != "" && value != argument
865
+ raise TemplateOperatorAbortedError,
866
+ "Value for template operator 'opt' was unexpected."
867
+ end
868
+ end
869
+
870
+ ##
871
+ # Extracts a URI Template neg operator.
872
+ #
873
+ # @param [String] value The unparsed value to extract from.
874
+ # @param [#restore] processor The processor object.
875
+ # @param [String] argument The argument to the operator.
876
+ # @param [Array] variables The variables the operator is working on.
877
+ # @param [Hash] mapping The mapping of variables to values.
878
+ #
879
+ # @return [String] The extracted result.
880
+ def extract_neg_operator(
881
+ value, processor, argument, variables, mapping)
882
+ if value != "" && value != argument
883
+ raise TemplateOperatorAbortedError,
884
+ "Value for template operator 'neg' was unexpected."
885
+ end
886
+ end
887
+
888
+ ##
889
+ # Extracts a URI Template prefix operator.
890
+ #
891
+ # @param [String] value The unparsed value to extract from.
892
+ # @param [#restore] processor The processor object.
893
+ # @param [String] argument The argument to the operator.
894
+ # @param [Array] variables The variables the operator is working on.
895
+ # @param [Hash] mapping The mapping of variables to values.
896
+ #
897
+ # @return [String] The extracted result.
898
+ def extract_prefix_operator(
899
+ value, processor, argument, variables, mapping)
900
+ if variables.size != 1
901
+ raise InvalidTemplateOperatorError,
902
+ "Template operator 'prefix' takes exactly one variable."
903
+ end
904
+ if value[0...argument.size] != argument
905
+ raise TemplateOperatorAbortedError,
906
+ "Value for template operator 'prefix' missing expected prefix."
907
+ end
908
+ values = value.split(argument, -1)
909
+ values << "" if value[-argument.size..-1] == argument
910
+ values.shift if values[0] == ""
911
+ values.pop if values[-1] == ""
912
+
913
+ if processor && processor.respond_to?(:restore)
914
+ values.map! { |value| processor.restore(variables.first, value) }
915
+ end
916
+ values = values.first if values.size == 1
917
+ if mapping[variables.first] == nil || mapping[variables.first] == values
918
+ mapping[variables.first] = values
919
+ else
920
+ raise TemplateOperatorAbortedError,
921
+ "Value mismatch for repeated variable."
922
+ end
923
+ end
924
+
925
+ ##
926
+ # Extracts a URI Template suffix operator.
927
+ #
928
+ # @param [String] value The unparsed value to extract from.
929
+ # @param [#restore] processor The processor object.
930
+ # @param [String] argument The argument to the operator.
931
+ # @param [Array] variables The variables the operator is working on.
932
+ # @param [Hash] mapping The mapping of variables to values.
933
+ #
934
+ # @return [String] The extracted result.
935
+ def extract_suffix_operator(
936
+ value, processor, argument, variables, mapping)
937
+ if variables.size != 1
938
+ raise InvalidTemplateOperatorError,
939
+ "Template operator 'suffix' takes exactly one variable."
940
+ end
941
+ if value[-argument.size..-1] != argument
942
+ raise TemplateOperatorAbortedError,
943
+ "Value for template operator 'suffix' missing expected suffix."
944
+ end
945
+ values = value.split(argument, -1)
946
+ values.pop if values[-1] == ""
947
+ if processor && processor.respond_to?(:restore)
948
+ values.map! { |value| processor.restore(variables.first, value) }
949
+ end
950
+ values = values.first if values.size == 1
951
+ if mapping[variables.first] == nil || mapping[variables.first] == values
952
+ mapping[variables.first] = values
953
+ else
954
+ raise TemplateOperatorAbortedError,
955
+ "Value mismatch for repeated variable."
956
+ end
957
+ end
958
+
959
+ ##
960
+ # Extracts a URI Template join operator.
961
+ #
962
+ # @param [String] value The unparsed value to extract from.
963
+ # @param [#restore] processor The processor object.
964
+ # @param [String] argument The argument to the operator.
965
+ # @param [Array] variables The variables the operator is working on.
966
+ # @param [Hash] mapping The mapping of variables to values.
967
+ #
968
+ # @return [String] The extracted result.
969
+ def extract_join_operator(value, processor, argument, variables, mapping)
970
+ unparsed_values = value.split(argument)
971
+ parsed_variables = []
972
+ for unparsed_value in unparsed_values
973
+ name = unparsed_value[/^(.+?)=(.+)$/, 1]
974
+ parsed_variables << name
975
+ parsed_value = unparsed_value[/^(.+?)=(.+)$/, 2]
976
+ if processor && processor.respond_to?(:restore)
977
+ parsed_value = processor.restore(name, parsed_value)
978
+ end
979
+ if mapping[name] == nil || mapping[name] == parsed_value
980
+ mapping[name] = parsed_value
981
+ else
982
+ raise TemplateOperatorAbortedError,
983
+ "Value mismatch for repeated variable."
984
+ end
985
+ end
986
+ for variable in variables
987
+ if !parsed_variables.include?(variable) && mapping[variable] != nil
988
+ raise TemplateOperatorAbortedError,
989
+ "Value mismatch for repeated variable."
990
+ end
991
+ end
992
+ if (parsed_variables & variables) != parsed_variables
993
+ raise TemplateOperatorAbortedError,
994
+ "Template operator 'join' variable mismatch: " +
995
+ "#{parsed_variables.inspect}, #{variables.inspect}"
996
+ end
997
+ end
998
+
999
+ ##
1000
+ # Extracts a URI Template list operator.
1001
+ #
1002
+ # @param [String] value The unparsed value to extract from.
1003
+ # @param [#restore] processor The processor object.
1004
+ # @param [String] argument The argument to the operator.
1005
+ # @param [Array] variables The variables the operator is working on.
1006
+ # @param [Hash] mapping The mapping of variables to values.
1007
+ #
1008
+ # @return [String] The extracted result.
1009
+ def extract_list_operator(value, processor, argument, variables, mapping)
1010
+ if variables.size != 1
1011
+ raise InvalidTemplateOperatorError,
1012
+ "Template operator 'list' takes exactly one variable."
1013
+ end
1014
+ values = value.split(argument, -1)
1015
+ values.pop if values[-1] == ""
1016
+ if processor && processor.respond_to?(:restore)
1017
+ values.map! { |value| processor.restore(variables.first, value) }
1018
+ end
1019
+ if mapping[variables.first] == nil || mapping[variables.first] == values
1020
+ mapping[variables.first] = values
1021
+ else
1022
+ raise TemplateOperatorAbortedError,
1023
+ "Value mismatch for repeated variable."
1024
+ end
1025
+ end
1026
+ end
1027
+ end