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

root/trunk/actionpack/lib/action_controller/routing.rb

Revision 5169, 43.7 kB (checked in by ulysses, 4 years ago)

Clear the cache of possible controllers whenever Routes are reloaded.

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