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, 9 months 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