Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/branches/1-2-stable/actionpack/lib/action_controller/routing.rb

Revision 7838, 47.1 kB (checked in by nzkoz, 2 years ago)

Backport fix to allow :controller=>:some_symbol [norbert]

Line 
1 require 'cgi'
2
3 class Object
4   def to_param
5     to_s
6   end
7 end
8
9 class TrueClass
10   def to_param
11     self
12   end
13 end
14
15 class FalseClass
16   def to_param
17     self
18   end
19 end
20
21 class NilClass
22   def to_param
23     self
24   end
25 end
26
27 class Regexp #:nodoc:
28   def number_of_captures
29     Regexp.new("|#{source}").match('').captures.length
30   end
31  
32   class << self
33     def optionalize(pattern)
34       case unoptionalize(pattern)
35         when /\A(.|\(.*\))\Z/ then "#{pattern}?"
36         else "(?:#{pattern})?"
37       end
38     end
39    
40     def unoptionalize(pattern)
41       [/\A\(\?:(.*)\)\?\Z/, /\A(.|\(.*\))\?\Z/].each do |regexp|
42         return $1 if regexp =~ pattern
43       end
44       return pattern
45     end
46   end
47 end
48
49 module ActionController
50   # == Routing
51   #
52   # The routing module provides URL rewriting in native Ruby. It's a way to
53   # redirect incoming requests to controllers and actions. This replaces
54   # mod_rewrite rules. Best of all Rails' Routing works with any web server.
55   # Routes are defined in routes.rb in your RAILS_ROOT/config directory.
56   #
57   # Consider the following route, installed by Rails when you generate your
58   # application:
59   #
60   #   map.connect ':controller/:action/:id'
61   #
62   # This route states that it expects requests to consist of a
63   # :controller followed by an :action that in turns is fed by some :id
64   #
65   # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end up
66   # with:
67   #
68   #   params = { :controller => 'blog',
69   #              :action     => 'edit'
70   #              :id         => '22'
71   #           }
72   #
73   # Think of creating routes as drawing a map for your requests. The map tells
74   # them where to go based on some predefined pattern:
75   #
76   #  ActionController::Routing::Routes.draw do |map|
77   #   Pattern 1 tells some request to go to one place
78   #   Pattern 2 tell them to go to another
79   #   ...
80   #  end
81   #
82   # The following symbols are special:
83   #
84   #   :controller maps to your controller name
85   #   :action     maps to an action with your controllers
86   #   
87   # Other names simply map to a parameter as in the case of +:id+.
88   #   
89   # == Route priority
90   #
91   # Not all routes are created equally. Routes have priority defined by the
92   # order of appearance of the routes in the routes.rb file. The priority goes
93   # from top to bottom. The last route in that file is at the lowest priority
94   # will be applied last. If no route matches, 404 is returned.
95   #
96   # Within blocks, the empty pattern goes first i.e. is at the highest priority.
97   # In practice this works out nicely:
98   #
99   #  ActionController::Routing::Routes.draw do |map|
100   #    map.with_options :controller => 'blog' do |blog|
101   #      blog.show    '',  :action => 'list'
102   #    end
103   #    map.connect ':controller/:action/:view
104   #  end
105   #
106   # In this case, invoking blog controller (with an URL like '/blog/')
107   # without parameters will activate the 'list' action by default.
108   #
109   # == Defaults routes and default parameters
110   #
111   # Setting a default route is straightforward in Rails because by appending a
112   # Hash to the end of your mapping you can set default parameters.
113   #
114   # Example:
115   #  ActionController::Routing:Routes.draw do |map|
116   #    map.connect ':controller/:action/:id', :controller => 'blog'
117   #  end
118   #
119   # This sets up  +blog+ as the default controller if no other is specified.
120   # This means visiting '/' would invoke the blog controller.
121   #
122   # More formally, you can define defaults in a route with the +:defaults+ key.
123   #   
124   #   map.connect ':controller/:id/:action', :action => 'show', :defaults => { :page => 'Dashboard' }
125   #
126   # == Named routes
127   #
128   # Routes can be named with the syntax <tt>map.name_of_route options</tt>,
129   # allowing for easy reference within your source as +name_of_route_url+
130   # for the full URL and +name_of_route_path+ for the URI path.
131   #
132   # Example:
133   #   # In routes.rb
134   #   map.login 'login', :controller => 'accounts', :action => 'login'
135   #
136   #   # With render, redirect_to, tests, etc.
137   #   redirect_to login_url
138   #
139   # Arguments can be passed as well.
140   #
141   #   redirect_to show_item_path(:id => 25)
142   #
143   # Use <tt>map.root</tt> as a shorthand to name a route for the root path ""
144   #
145   #   # In routes.rb
146   #   map.root :controller => 'blogs'
147   #
148   #   # would recognize http://www.example.com/ as
149   #   params = { :controller => 'blogs', :action => 'index' }
150   #
151   #   # and provide these named routes
152   #   root_url   # => 'http://www.example.com/'
153   #   root_path  # => ''
154   #
155   # Note: when using +with_options+, the route is simply named after the
156   # method you call on the block parameter rather than map.
157   #
158   #   # In routes.rb
159   #   map.with_options :controller => 'blog' do |blog|
160   #     blog.show    '',            :action  => 'list'
161   #     blog.delete  'delete/:id',  :action  => 'delete',
162   #     blog.edit    'edit/:id',    :action  => 'edit'
163   #   end
164   #
165   #   # provides named routes for show, delete, and edit
166   #   link_to @article.title, show_path(:id => @article.id)
167   #
168   # == Pretty URLs
169   #
170   # Routes can generate pretty URLs. For example:
171   #
172   #  map.connect 'articles/:year/:month/:day',
173   #              :controller => 'articles',
174   #              :action     => 'find_by_date',
175   #              :year       => /\d{4}/,
176   #              :month => /\d{1,2}/,
177   #              :day   => /\d{1,2}/
178  
179   #  # Using the route above, the url below maps to:
180   #  # params = {:year => '2005', :month => '11', :day => '06'}
181   #  # http://localhost:3000/articles/2005/11/06
182   #
183   # == Regular Expressions and parameters
184   # You can specify a reqular expression to define a format for a parameter.
185   #
186   #  map.geocode 'geocode/:postalcode', :controller => 'geocode',
187   #              :action => 'show', :postalcode => /\d{5}(-\d{4})?/
188   #
189   # or  more formally:
190   #
191   #   map.geocode 'geocode/:postalcode', :controller => 'geocode',
192   #                      :action => 'show',
193   #                      :requirements { :postalcode => /\d{5}(-\d{4})?/ }
194   #
195   # == Route globbing
196   #
197   # Specifying <tt>*[string]</tt> as part of a rule like :
198   #
199   #  map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?'
200   #
201   # will glob all remaining parts of the route that were not recognized earlier. This idiom must appear at the end of the path. The globbed values are in <tt>params[:path]</tt> in this case. 
202   #
203   # == Reloading routes
204   #
205   # You can reload routes if you feel you must:
206   #
207   #  Action::Controller::Routes.reload
208   #
209   # This will clear all named routes and reload routes.rb
210   #
211   # == Testing Routes
212   #
213   # The two main methods for testing your routes:
214   #
215   # === +assert_routing+
216   #
217   #  def test_movie_route_properly_splits
218   #   opts = {:controller => "plugin", :action => "checkout", :id => "2"}
219   #   assert_routing "plugin/checkout/2", opts
220   #  end
221  
222   # +assert_routing+ lets you test whether or not the route properly resolves into options.
223   #
224   # === +assert_recognizes+
225   #
226   #  def test_route_has_options
227   #   opts = {:controller => "plugin", :action => "show", :id => "12"}
228   #   assert_recognizes opts, "/plugins/show/12"
229   #  end
230   #
231   # Note the subtle difference between the two: +assert_routing+ tests that
232   # an URL fits options while +assert_recognizes+ tests that an URL
233   # breaks into parameters properly.
234   #
235   # In tests you can simply pass the URL or named route to +get+ or +post+.
236   #
237   #  def send_to_jail
238   #    get '/jail'
239   #    assert_response :success
240   #    assert_template "jail/front"
241   #  end
242   #
243   #  def goes_to_login
244   #    get login_url
245   #    #...
246   #  end
247   #
248   module Routing
249     SEPARATORS = %w( / ; . , ? )
250
251     # The root paths which may contain controller files
252     mattr_accessor :controller_paths
253     self.controller_paths = []
254
255     class << self
256       def with_controllers(names)
257         prior_controllers = @possible_controllers
258         use_controllers! names
259         yield
260       ensure
261         use_controllers! prior_controllers
262       end
263
264       def normalize_paths(paths)
265         # do the hokey-pokey of path normalization...
266         paths = paths.collect do |path|
267           path = path.
268             gsub("//", "/").           # replace double / chars with a single
269             gsub("\\\\", "\\").        # replace double \ chars with a single
270             gsub(%r{(.)[\\/]$}, '\1')  # drop final / or \ if path ends with it
271
272           # eliminate .. paths where possible
273           re = %r{\w+[/\\]\.\.[/\\]}
274           path.gsub!(%r{\w+[/\\]\.\.[/\\]}, "") while path.match(re)
275           path
276         end
277
278         # start with longest path, first
279         paths = paths.uniq.sort_by { |path| - path.length }
280       end
281
282       def possible_controllers
283         unless @possible_controllers
284           @possible_controllers = []
285        
286           paths = controller_paths.select { |path| File.directory?(path) && path != "." }
287
288           seen_paths = Hash.new {|h, k| h[k] = true; false}
289           normalize_paths(paths).each do |load_path|
290             Dir["#{load_path}/**/*_controller.rb"].collect do |path|
291               next if seen_paths[path.gsub(%r{^\.[/\\]}, "")]
292              
293               controller_name = path[(load_path.length + 1)..-1]
294              
295               controller_name.gsub!(/_controller\.rb\Z/, '')
296               @possible_controllers << controller_name
297             end
298           end
299
300           # remove duplicates
301           @possible_controllers.uniq!
302         end
303         @possible_controllers
304       end
305
306       def use_controllers!(controller_names)
307         @possible_controllers = controller_names
308       end
309
310       def controller_relative_to(controller, previous)
311         if controller.nil?           then previous
312         elsif controller[0] == ?/    then controller[1..-1]
313         elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}"
314         else controller
315         end     
316       end     
317     end
318  
319     class Route #:nodoc:
320       attr_accessor :segments, :requirements, :conditions
321      
322       def initialize
323         @segments = []
324         @requirements = {}
325         @conditions = {}
326       end
327  
328       # Write and compile a +generate+ method for this Route.
329       def write_generation
330         # Build the main body of the generation
331         body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
332    
333         # If we have conditions that must be tested first, nest the body inside an if
334         body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
335         args = "options, hash, expire_on = {}"
336
337         # Nest the body inside of a def block, and then compile it.
338         raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
339         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
340
341         # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
342         # are the same as the keys that were recalled from the previous request. Thus,
343         # we can use the expire_on.keys to determine which keys ought to be used to build
344         # the query string. (Never use keys from the recalled request when building the
345         # query string.)
346
347         method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
348         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
349
350         method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
351         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
352         raw_method
353       end
354  
355       # Build several lines of code that extract values from the options hash. If any
356       # of the values are missing or rejected then a return will be executed.
357       def generation_extraction
358         segments.collect do |segment|
359           segment.extraction_code
360         end.compact * "\n"
361       end
362  
363       # Produce a condition expression that will check the requirements of this route
364       # upon generation.
365       def generation_requirements
366         requirement_conditions = requirements.collect do |key, req|
367           if req.is_a? Regexp
368             value_regexp = Regexp.new "\\A#{req.source}\\Z"
369             "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
370           else
371             "hash[:#{key}] == #{req.inspect}"
372           end
373         end
374         requirement_conditions * ' && ' unless requirement_conditions.empty?
375       end
376       def generation_structure
377         segments.last.string_structure segments[0..-2]
378       end
379  
380       # Write and compile a +recognize+ method for this Route.
381       def write_recognition
382         # Create an if structure to extract the params from a match if it occurs.
383         body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
384         body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
385    
386         # Build the method declaration and compile it
387         method_decl = "def recognize(path, env={})\n#{body}\nend"
388         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
389         method_decl
390       end
391
392       # Plugins may override this method to add other conditions, like checks on
393       # host, subdomain, and so forth. Note that changes here only affect route
394       # recognition, not generation.
395       def recognition_conditions
396         result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
397         result << "conditions[:method] === env[:method]" if conditions[:method]
398         result
399       end
400
401       # Build the regular expression pattern that will match this route.
402       def recognition_pattern(wrap = true)
403         pattern = ''
404         segments.reverse_each do |segment|
405           pattern = segment.build_pattern pattern
406         end
407         wrap ? ("\\A" + pattern + "\\Z") : pattern
408       end
409      
410       # Write the code to extract the parameters from a matched route.
411       def recognition_extraction
412         next_capture = 1
413         extraction = segments.collect do |segment|
414           x = segment.match_extraction next_capture
415           next_capture += Regexp.new(segment.regexp_chunk).number_of_captures
416           x
417         end
418         extraction.compact
419       end
420  
421       # Write the real generation implementation and then resend the message.
422       def generate(options, hash, expire_on = {})
423         write_generation
424         generate options, hash, expire_on
425       end
426
427       def generate_extras(options, hash, expire_on = {})
428         write_generation
429         generate_extras options, hash, expire_on
430       end
431
432       # Generate the query string with any extra keys in the hash and append
433       # it to the given path, returning the new path.
434       def append_query_string(path, hash, query_keys=nil)
435         return nil unless path
436         query_keys ||= extra_keys(hash)
437         "#{path}#{build_query_string(hash, query_keys)}"
438       end
439
440       # Determine which keys in the given hash are "extra". Extra keys are
441       # those that were not used to generate a particular route. The extra
442       # keys also do not include those recalled from the prior request, nor
443       # do they include any keys that were implied in the route (like a
444       # :controller that is required, but not explicitly used in the text of
445       # the route.)
446       def extra_keys(hash, recall={})
447         (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
448       end
449
450       # Build a query string from the keys of the given hash. If +only_keys+
451       # is given (as an array), only the keys indicated will be used to build
452       # the query string. The query string will correctly build array parameter
453       # values.
454       def build_query_string(hash, only_keys = nil)
455         elements = []
456        
457         (only_keys || hash.keys).each do |key|
458           if value = hash[key]
459             elements << value.to_query(key)
460           end
461         end
462         elements.empty? ? '' : "?#{elements.sort * '&'}"
463       end
464
465       # Write the real recognition implementation and then resend the message.
466       def recognize(path, environment={})
467         write_recognition
468         recognize path, environment
469       end
470  
471       # A route's parameter shell contains parameter values that are not in the
472       # route's path, but should be placed in the recognized hash.
473       #
474       # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route:
475       #
476       #   map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
477       #
478       def parameter_shell
479         @parameter_shell ||= returning({}) do |shell|
480           requirements.each do |key, requirement|
481             shell[key] = requirement unless requirement.is_a? Regexp
482           end
483         end
484       end
485  
486       # Return an array containing all the keys that are used in this route. This
487       # includes keys that appear inside the path, and keys that have requirements
488       # placed upon them.
489       def significant_keys
490         @significant_keys ||= returning [] do |sk|
491           segments.each { |segment| sk << segment.key if segment.respond_to? :key }
492           sk.concat requirements.keys
493           sk.uniq!
494         end
495       end
496
497       # Return a hash of key/value pairs representing the keys in the route that
498       # have defaults, or which are specified by non-regexp requirements.
499       def defaults
500         @defaults ||= returning({}) do |hash|
501           segments.each do |segment|
502             next unless segment.respond_to? :default
503             hash[segment.key] = segment.default unless segment.default.nil?
504           end
505           requirements.each do |key,req|
506             next if Regexp === req || req.nil?
507             hash[key] = req
508           end
509         end
510       end
511  
512       def matches_controller_and_action?(controller, action)
513         unless @matching_prepared
514           @controller_requirement = requirement_for(:controller)
515           @action_requirement = requirement_for(:action)
516           @matching_prepared = true
517         end
518
519         (@controller_requirement.nil? || @controller_requirement === controller) &&
520         (@action_requirement.nil? || @action_requirement === action)
521       end
522
523       def to_s
524         @to_s ||= begin
525           segs = segments.inject("") { |str,s| str << s.to_s }
526           "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
527         end
528       end
529  
530     protected
531       def requirement_for(key)
532         return requirements[key] if requirements.key? key
533         segments.each do |segment|
534           return segment.regexp if segment.respond_to?(:key) && segment.key == key
535         end
536         nil
537       end
538  
539     end
540
541     class Segment #:nodoc:
542       attr_accessor :is_optional
543       alias_method :optional?, :is_optional
544
545       def initialize
546         self.is_optional = false
547       end
548
549       def extraction_code
550         nil
551       end
552  
553       # Continue generating string for the prior segments.
554       def continue_string_structure(prior_segments)
555         if prior_segments.empty?
556           interpolation_statement(prior_segments)
557         else
558           new_priors = prior_segments[0..-2]
559           prior_segments.last.string_structure(new_priors)
560         end
561       end
562  
563       # Return a string interpolation statement for this segment and those before it.
564       def interpolation_statement(prior_segments)
565         chunks = prior_segments.collect { |s| s.interpolation_chunk }
566         chunks << interpolation_chunk
567         "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}"
568       end
569  
570       def string_structure(prior_segments)
571         optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments)
572       end
573  
574       # Return an if condition that is true if all the prior segments can be generated.
575       # If there are no optional segments before this one, then nil is returned.
576       def all_optionals_available_condition(prior_segments)
577         optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact
578         optional_locals.empty? ? nil : " if #{optional_locals * ' && '}"
579       end
580  
581       # Recognition
582  
583       def match_extraction(next_capture)
584         nil
585       end
586  
587       # Warning
588  
589       # Returns true if this segment is optional? because of a default. If so, then
590       # no warning will be emitted regarding this segment.
591       def optionality_implied?
592         false
593       end
594     end
595
596     class StaticSegment < Segment #:nodoc:
597       attr_accessor :value, :raw
598       alias_method :raw?, :raw
599  
600       def initialize(value = nil)
601         super()
602         self.value = value
603       end
604  
605       def interpolation_chunk
606         raw? ? value : CGI.escape(value)
607       end
608  
609       def regexp_chunk
610         chunk = Regexp.escape value
611         optional? ? Regexp.optionalize(chunk) : chunk
612       end
613  
614       def build_pattern(pattern)
615         escaped = Regexp.escape(value)
616         if optional? && ! pattern.empty?
617           "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})"
618         elsif optional?
619           Regexp.optionalize escaped
620         else
621           escaped + pattern
622         end
623       end
624  
625       def to_s
626         value
627       end
628     end
629
630     class DividerSegment < StaticSegment #:nodoc:
631       def initialize(value = nil)
632         super(value)
633         self.raw = true
634         self.is_optional = true
635       end
636  
637       def optionality_implied?
638         true
639       end
640     end
641
642     class DynamicSegment < Segment #:nodoc:
643       attr_accessor :key, :default, :regexp
644  
645       def initialize(key = nil, options = {})
646         super()
647         self.key = key
648         self.default = options[:default] if options.key? :default
649         self.is_optional = true if options[:optional] || options.key?(:default)
650       end
651  
652       def to_s
653         ":#{key}"
654       end
655  
656       # The local variable name that the value of this segment will be extracted to.
657       def local_name
658         "#{key}_value"
659       end
660  
661       def extract_value
662         "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}"
663       end
664       def value_check
665         if default # Then we know it won't be nil
666           "#{value_regexp.inspect} =~ #{local_name}" if regexp
667         elsif optional?
668           # If we have a regexp check that the value is not given, or that it matches.
669           # If we have no regexp, return nil since we do not require a condition.
670           "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp
671         else # Then it must be present, and if we have a regexp, it must match too.
672           "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}"
673         end
674       end
675       def expiry_statement
676         "expired, hash = true, options if !expired && expire_on[:#{key}]"
677       end
678  
679       def extraction_code
680         s = extract_value
681         vc = value_check
682         s << "\nreturn [nil,nil] unless #{vc}" if vc
683         s << "\n#{expiry_statement}"
684       end
685  
686       def interpolation_chunk
687         "\#{CGI.escape(#{local_name}.to_s)}"
688       end
689  
690       def string_structure(prior_segments)
691         if optional? # We have a conditional to do...
692           # If we should not appear in the url, just write the code for the prior
693           # segments. This occurs if our value is the default value, or, if we are
694           # optional, if we have nil as our value.
695           "if #{local_name} == #{default.inspect}\n" +
696             continue_string_structure(prior_segments) +
697           "\nelse\n" + # Otherwise, write the code up to here
698             "#{interpolation_statement(prior_segments)}\nend"
699         else
700           interpolation_statement(prior_segments)
701         end
702       end
703  
704       def value_regexp
705         Regexp.new "\\A#{regexp.source}\\Z" if regexp
706       end
707       def regexp_chunk
708         regexp ? "(#{regexp.source})" : "([^#{Routing::SEPARATORS.join}]+)"
709       end
710  
711       def build_pattern(pattern)
712         chunk = regexp_chunk
713         chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0
714         pattern = "#{chunk}#{pattern}"
715         optional? ? Regexp.optionalize(pattern) : pattern
716       end
717       def match_extraction(next_capture)
718         hangon = (default ? "|| #{default.inspect}" : "if match[#{next_capture}]")
719        
720         # All non code-related keys (such as :id, :slug) have to be unescaped as other CGI params
721         "params[:#{key}] = match[#{next_capture}] #{hangon}"
722       end
723  
724       def optionality_implied?
725         [:action, :id].include? key
726       end
727  
728     end
729
730     class ControllerSegment < DynamicSegment #:nodoc:
731       def regexp_chunk
732         possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name }
733         "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
734       end
735
736       # Don't CGI.escape the controller name, since it may have slashes in it,
737       # like admin/foo.
738       def interpolation_chunk
739         "\#{#{local_name}.to_s}"
740       end
741
742       # Make sure controller names like Admin/Content are correctly normalized to
743       # admin/content
744       def extract_value
745         "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase"
746       end
747
748       def match_extraction(next_capture)
749         if default
750           "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'"
751         else
752           "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]"
753         end
754       end
755     end
756
757     class PathSegment < DynamicSegment #:nodoc:
758       EscapedSlash = CGI.escape("/")
759       def interpolation_chunk
760         "\#{CGI.escape(#{local_name}.to_s).gsub(#{EscapedSlash.inspect}, '/')}"
761       end
762
763       def default
764         ''
765       end
766
767       def default=(path)
768         raise RoutingError, "paths cannot have non-empty default values" unless path.blank?
769       end
770
771       def match_extraction(next_capture)
772         "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}"
773       end
774
775       def regexp_chunk
776         regexp || "(.*)"
777       end
778
779       class Result < ::Array #:nodoc:
780         def to_s() join '/' end
781         def self.new_escaped(strings)
782           new strings.collect {|str| CGI.unescape str}
783         end     
784       end     
785     end
786
787     class RouteBuilder #:nodoc:
788       attr_accessor :separators, :optional_separators
789  
790       def initialize
791         self.separators = Routing::SEPARATORS
792         self.optional_separators = %w( / )
793       end
794  
795       def separator_pattern(inverted = false)
796         "[#{'^' if inverted}#{Regexp.escape(separators.join)}]"
797       end
798  
799       def interval_regexp
800         Regexp.new "(.*?)(#{separators.source}|$)"
801       end
802  
803       # Accepts a "route path" (a string defining a route), and returns the array
804       # of segments that corresponds to it. Note that the segment array is only
805       # partially initialized--the defaults and requirements, for instance, need
806       # to be set separately, via the #assign_route_options method, and the
807       # #optional? method for each segment will not be reliable until after
808       # #assign_route_options is called, as well.
809       def segments_for_route_path(path)
810         rest, segments = path, []
811    
812         until rest.empty?
813           segment, rest = segment_for rest
814           segments << segment
815         end
816         segments
817       end
818
819       # A factory method that returns a new segment instance appropriate for the
820       # format of the given string.
821       def segment_for(string)
822         segment = case string
823           when /\A:(\w+)/
824             key = $1.to_sym
825             case key
826               when :controller then ControllerSegment.new(key)
827               else DynamicSegment.new key
828             end
829           when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true)
830           when /\A\?(.*?)\?/
831             returning segment = StaticSegment.new($1) do
832               segment.is_optional = true
833             end
834           when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1)
835           when Regexp.new(separator_pattern) then
836             returning segment = DividerSegment.new($&) do
837               segment.is_optional = (optional_separators.include? $&)
838             end
839         end
840         [segment, $~.post_match]
841       end
842  
843       # Split the given hash of options into requirement and default hashes. The
844       # segments are passed alongside in order to distinguish between default values
845       # and requirements.
846       def divide_route_options(segments, options)
847         options = options.dup
848         requirements = (options.delete(:requirements) || {}).dup
849         defaults     = (options.delete(:defaults)     || {}).dup
850         conditions   = (options.delete(:conditions)   || {}).dup
851
852         path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact
853         options.each do |key, value|
854           hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements
855           hash[key] = value
856         end
857    
858         [defaults, requirements, conditions]
859       end
860      
861       # Takes a hash of defaults and a hash of requirements, and assigns them to
862       # the segments. Any unused requirements (which do not correspond to a segment)
863       # are returned as a hash.
864       def assign_route_options(segments, defaults, requirements)
865         route_requirements = {} # Requirements that do not belong to a segment
866        
867         segment_named = Proc.new do |key|
868           segments.detect { |segment| segment.key == key if segment.respond_to?(:key) }
869         end
870        
871         requirements.each do |key, requirement|
872           segment = segment_named[key]
873           if segment
874             raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp)
875             if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
876               raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
877             end
878             segment.regexp = requirement
879           else
880             route_requirements[key] = requirement
881           end
882         end
883        
884         defaults.each do |key, default|
885           segment = segment_named[key]
886           raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment
887           segment.is_optional = true
888           segment.default = default.to_param if default
889         end
890        
891         assign_default_route_options(segments)
892         ensure_required_segments(segments)
893         route_requirements
894       end
895      
896       # Assign default options, such as 'index' as a default for :action. This
897       # method must be run *after* user supplied requirements and defaults have
898       # been applied to the segments.
899       def assign_default_route_options(segments)
900         segments.each do |segment|
901           next unless segment.is_a? DynamicSegment
902           case segment.key
903             when :action
904               if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index'
905                 segment.default ||= 'index'
906                 segment.is_optional = true
907               end
908             when :id
909               if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ ''
910                 segment.is_optional = true
911               end
912           end
913         end
914       end
915      
916       # Makes sure that there are no optional segments that precede a required
917       # segment. If any are found that precede a required segment, they are
918       # made required.
919       def ensure_required_segments(segments)
920         allow_optional = true
921         segments.reverse_each do |segment|
922           allow_optional &&= segment.optional?
923           if !allow_optional && segment.optional?
924             unless segment.optionality_implied?
925               warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required."
926             end
927             segment.is_optional = false
928           elsif allow_optional & segment.respond_to?(:default) && segment.default
929             # if a segment has a default, then it is optional
930             segment.is_optional = true
931           end
932         end
933       end
934      
935       # Construct and return a route with the given path and options.
936       def build(path, options)
937         # Wrap the path with slashes
938         path = "/#{path}" unless path[0] == ?/
939         path = "#{path}/" unless path[-1] == ?/
940    
941         segments = segments_for_route_path(path)
942         defaults, requirements, conditions = divide_route_options(segments, options)
943         requirements = assign_route_options(segments, defaults, requirements)
944
945         route = Route.new
946         route.segments = segments
947         route.requirements = requirements
948         route.conditions = conditions
949
950         if !route.significant_keys.include?(:action) && !route.requirements[:action]
951           route.requirements[:action] = "index"
952           route.significant_keys << :action
953         end
954
955         if !route.significant_keys.include?(:controller)
956           raise ArgumentError, "Illegal route: the :controller must be specified!"
957         end
958
959         route
960       end
961     end
962
963     class RouteSet #:nodoc:
964       # Mapper instances are used to build routes. The object passed to the draw
965       # block in config/routes.rb is a Mapper instance.
966       #
967       # Mapper instances have relatively few instance methods, in order to avoid
968       # clashes with named routes.
969       class Mapper #:nodoc:
970         def initialize(set)
971           @set = set
972         end
973    
974         # Create an unnamed route with the provided +path+ and +options+. See
975         # SomeHelpfulUrl for an introduction to routes.
976         def connect(path, options = {})
977           @set.add_route(path, options)
978         end
979
980         def named_route(name, path, options = {})
981           @set.add_named_route(name, path, options)
982         end
983        
984         def deprecated_named_route(name, deprecated_name, options = {})
985           @set.add_deprecated_named_route(name, deprecated_name)
986         end
987
988         # Added deprecation notice for anyone who already added a named route called "root".
989         # It'll be used as a shortcut for map.connect '' in Rails 2.0.
990         def root(*args, &proc)
991           super unless args.length >= 1 && proc.nil?
992           @set.add_named_route("root", *args)
993         end
994         deprecate :root => "(as the the label for a named route) will become a shortcut for map.connect '', so find another name"
995
996         def method_missing(route_name, *args, &proc)
997           super unless args.length >= 1 && proc.nil?
998           @set.add_named_route(route_name, *args)
999         end
1000       end
1001
1002       # A NamedRouteCollection instance is a collection of named routes, and also
1003       # maintains an anonymous module that can be used to install helpers for the
1004       # named routes.
1005       class NamedRouteCollection #:nodoc:
1006         include Enumerable
1007
1008         attr_reader :routes, :helpers
1009
1010         def initialize
1011           clear!
1012         end
1013
1014         def clear!
1015           @routes = {}
1016           @helpers = []
1017
1018           @module ||= Module.new
1019           @module.instance_methods.each do |selector|
1020             @module.send :remove_method, selector
1021           end
1022         end
1023
1024         def add(name, route)
1025           routes[name.to_sym] = route
1026           define_named_route_methods(name, route)
1027         end
1028
1029         def get(name)
1030           routes[name.to_sym]
1031         end
1032
1033         alias []=   add
1034         alias []    get
1035         alias clear clear!
1036
1037         def each
1038           routes.each { |name, route| yield name, route }
1039           self
1040         end
1041
1042         def names
1043           routes.keys
1044         end
1045
1046         def length
1047           routes.length
1048         end
1049
1050         def install(destinations = [ActionController::Base, ActionView::Base])
1051           Array(destinations).each { |dest| dest.send :include, @module }
1052         end
1053        
1054         def define_deprecated_named_route_methods(name, deprecated_name)
1055
1056           [:url, :path].each do |kind|
1057             @module.send :module_eval, <<-end_eval # We use module_eval to avoid leaks
1058
1059               def #{url_helper_name(deprecated_name, kind)}(*args)
1060
1061                 ActiveSupport::Deprecation.warn(
1062                   'The named route "#{url_helper_name(deprecated_name, kind)}" uses a format that has been deprecated. ' +
1063                   'You should use "#{url_helper_name(name, kind)}" instead.', caller
1064                 )
1065
1066                 send :#{url_helper_name(name, kind)}, *args
1067
1068               end
1069
1070               def #{hash_access_name(deprecated_name, kind)}(*args)
1071
1072                 ActiveSupport::Deprecation.warn(
1073                   'The named route "#{hash_access_name(deprecated_name, kind)}" uses a format that has been deprecated. ' +
1074                   'You should use "#{hash_access_name(name, kind)}" instead.', caller
1075                 )
1076
1077                 send :#{hash_access_name(name, kind)}, *args
1078
1079               end
1080
1081             end_eval
1082           end
1083
1084         end
1085
1086         private
1087           def url_helper_name(name, kind = :url)
1088             :"#{name}_#{kind}"
1089           end
1090
1091           def hash_access_name(name, kind = :url)
1092             :"hash_for_#{name}_#{kind}"
1093           end
1094
1095           def define_named_route_methods(name, route)
1096             {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
1097               hash = route.defaults.merge(:use_route => name).merge(opts)
1098               define_hash_access route, name, kind, hash
1099               define_url_helper route, name, kind, hash
1100             end
1101           end
1102          
1103           def define_hash_access(route, name, kind, options)
1104             selector = hash_access_name(name, kind)
1105             @module.send :module_eval, <<-end_eval # We use module_eval to avoid leaks
1106               def #{selector}(options = nil)
1107                 options ? #{options.inspect}.merge(options) : #{options.inspect}
1108               end
1109             end_eval
1110             @module.send(:protected, selector)
1111             helpers << selector
1112           end
1113          
1114           def define_url_helper(route, name, kind, options)
1115             selector = url_helper_name(name, kind)
1116            
1117             # The segment keys used for positional paramters
1118             segment_keys = route.segments.collect do |segment|
1119               segment.key if segment.respond_to? :key
1120             end.compact
1121             hash_access_method = hash_access_name(name, kind)
1122            
1123             @module.send :module_eval, <<-end_eval # We use module_eval to avoid leaks
1124               def #{selector}(*args)
1125                 opts = if args.empty? || Hash === args.first
1126                   args.first || {}
1127                 else
1128                   # allow ordered parameters to be associated with corresponding
1129                   # dynamic segments, so you can do
1130                   #
1131                   #   foo_url(bar, baz, bang)
1132                   #
1133                   # instead of
1134                   #
1135                   #   foo_url(:bar => bar, :baz => baz, :bang => bang)
1136                   args.zip(#{segment_keys.inspect}).inject({}) do |h, (v, k)|
1137                     h[k] = v
1138                     h
1139                   end
1140                 end
1141                
1142                 url_for(#{hash_access_method}(opts))
1143               end
1144             end_eval
1145             @module.send(:protected, selector)
1146             helpers << selector
1147           end
1148          
1149       end
1150  
1151       attr_accessor :routes, :named_routes
1152  
1153       def initialize
1154         self.routes = []
1155         self.named_routes = NamedRouteCollection.new
1156       end
1157
1158       # Subclasses and plugins may override this method to specify a different
1159       # RouteBuilder instance, so that other route DSL's can be created.
1160       def builder
1161         @builder ||= RouteBuilder.new
1162       end
1163
1164       def draw
1165         clear!
1166         yield Mapper.new(self)
1167         named_routes.install
1168       end
1169  
1170       def clear!
1171         routes.clear
1172         named_routes.clear
1173         @combined_regexp = nil
1174         @routes_by_controller = nil
1175       end
1176
1177       def empty?
1178         routes.empty?
1179       end
1180  
1181       def load!
1182         Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
1183         clear!
1184         load_routes!
1185         named_routes.install
1186       end
1187
1188       alias reload load!
1189
1190       def load_routes!
1191         if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes
1192           load File.join("#{RAILS_ROOT}/config/routes.rb")
1193         else
1194           add_route ":controller/:action/:id"
1195         end
1196       end
1197  
1198       def add_route(path, options = {})
1199         route = builder.build(path, options)
1200         routes << route
1201         route
1202       end
1203  
1204       def add_named_route(name, path, options = {})
1205         named_routes[name] = add_route(path, options)
1206       end
1207      
1208       def add_deprecated_named_route(name, deprecated_name)
1209         named_routes.define_deprecated_named_route_methods(name, deprecated_name)
1210       end
1211  
1212       def options_as_params(options)
1213         # If an explicit :controller was given, always make :action explicit
1214         # too, so that action expiry works as expected for things like
1215         #
1216         #   generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
1217         #
1218         # (the above is from the unit tests). In the above case, because the
1219         # controller was explicitly given, but no action, the action is implied to
1220         # be "index", not the recalled action of "show".
1221         #
1222         # great fun, eh?
1223
1224         options_as_params = options.clone
1225         options_as_params[:action] ||= 'index' if options[:controller]
1226         options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action]
1227         options_as_params
1228       end
1229  
1230       def build_expiry(options, recall)
1231         recall.inject({}) do |expiry, (key, recalled_value)|
1232           expiry[key] = (options.key?(key) && options[key] != recalled_value)
1233           expiry
1234         end
1235       end
1236
1237       # Generate the path indicated by the arguments, and return an array of
1238       # the keys that were not used to generate it.
1239       def extra_keys(options, recall={})
1240         generate_extras(options, recall).last
1241       end
1242
1243       def generate_extras(options, recall={})
1244         generate(options, recall, :generate_extras)
1245       end
1246
1247       def generate(options, recall = {}, method=:generate)
1248         named_route_name = options.delete(:use_route)
1249         if named_route_name
1250           named_route = named_routes[named_route_name]
1251           options = named_route.parameter_shell.merge(options)
1252         end
1253
1254         options = options_as_params(options)
1255         expire_on = build_expiry(options, recall)
1256
1257         if options[:controller]
1258           options[:controller] = options[:controller].to_s
1259         end
1260         # if the controller has changed, make sure it changes relative to the
1261         # current controller module, if any. In other words, if we're currently
1262         # on admin/get, and the new controller is 'set', the new controller
1263         # should really be admin/set.
1264         if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
1265           old_parts = recall[:controller].split('/')
1266           new_parts = options[:controller].split('/')
1267           parts = old_parts[0..-(new_parts.length + 1)] + new_parts
1268           options[:controller] = parts.join('/')
1269         end
1270
1271         # drop the leading '/' on the controller name
1272         options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/
1273         merged = recall.merge(options)
1274
1275         if named_route
1276           path = named_route.generate(options, merged, expire_on)
1277           if path.nil?
1278             raise_named_route_error(options, named_route, named_route_name)
1279           else
1280             return path
1281           end
1282         else
1283           merged[:action] ||= 'index'
1284           options[:action] ||= 'index'
1285  
1286           controller = merged[:controller]
1287           action = merged[:action]
1288
1289           raise RoutingError, "Need controller and action!" unless controller && action
1290           # don't use the recalled keys when determining which routes to check
1291           routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]
1292
1293           routes.each do |route|
1294             results = route.send(method, options, merged, expire_on)
1295             return results if results && (!results.is_a?(Array) || results.first)
1296           end
1297         end
1298    
1299         raise RoutingError, "No route matches #{options.inspect}"
1300       end
1301      
1302       # try to give a helpful error message when named route generation fails
1303       def raise_named_route_error(options, named_route, named_route_name)
1304         diff = named_route.requirements.diff(options)
1305         unless diff.empty?
1306           raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}"
1307         else
1308           required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) }
1309           required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment
1310           raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route.  content_url has the following required parameters: #{required_keys_or_values.inspect} - are they all satisifed?"
1311         end
1312       end
1313  
1314       def recognize(request)
1315         params = recognize_path(request.path, extract_request_environment(request))
1316         request.path_parameters = params.with_indifferent_access
1317         "#{params[:controller].camelize}Controller".constantize
1318       end
1319  
1320       def recognize_path(path, environment={})
1321         path = CGI.unescape(path)
1322         routes.each do |route|
1323           result = route.recognize(path, environment) and return result
1324         end
1325         raise RoutingError, "no route found to match #{path.inspect} with #{environment.inspect}"
1326       end
1327  
1328       def routes_by_controller
1329         @routes_by_controller ||= Hash.new do |controller_hash, controller|
1330           controller_hash[controller] = Hash.new do |action_hash, action|
1331             action_hash[action] = Hash.new do |key_hash, keys|
1332               key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys)
1333             end
1334           end
1335         end
1336       end
1337  
1338       def routes_for(options, merged, expire_on)
1339         raise "Need controller and action!" unless controller && action
1340         controller = merged[:controller]
1341         merged = options if expire_on[:controller]
1342         action = merged[:action] || 'index'
1343    
1344         routes_by_controller[controller][action][merged.keys]
1345       end
1346  
1347       def routes_for_controller_and_action(controller, action)
1348         selected = routes.select do |route|
1349           route.matches_controller_and_action? controller, action
1350         end
1351         (selected.length == routes.length) ? routes : selected
1352       end
1353  
1354       def routes_for_controller_and_action_and_keys(controller, action, keys)
1355         selected = routes.select do |route|
1356           route.matches_controller_and_action? controller, action
1357         end
1358         selected.sort_by do |route|
1359           (keys - route.significant_keys).length
1360         end
1361       end
1362
1363       # Subclasses and plugins may override this method to extract further attributes
1364       # from the request, for use by route conditions and such.
1365       def extract_request_environment(request)
1366         { :method => request.method }
1367       end
1368     end
1369
1370     Routes = RouteSet.new
1371   end
1372 end
1373
Note: See TracBrowser for help on using the browser.