mislav-addressable 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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