Addressable C0 Coverage Information - RCov

lib/addressable/template.rb

Name Total Lines Lines of Code Total Coverage Code Coverage
lib/addressable/template.rb 1047 529
100.00%
100.00%

Key

Code reported as executed by Ruby looks like this...and this: this line is also marked as covered.Lines considered as run by rcov, but not reported by Ruby, look like this,and this: these lines were inferred by rcov (using simple heuristics).Finally, here's a line marked as not executed.

Coverage Details

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

Generated on Thu Feb 16 15:34:49 +0300 2012 with rcov 0.9.8