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

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

Revision 5304, 43.6 kB (checked in by minam, 4 years ago)

remove an obsolete #dup call. avoid double negatives, to make the code easier to understand and explain

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 = "expired = false\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         "expired, hash = true, options if !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(destinations = [ActionController::Base, ActionView::Base])
1029           Array(destinations).each { |dest| dest.send :include, @module }
1030         end
1031
1032         private
1033           def url_helper_name(name, kind = :url)
1034             :"#{name}_#{kind}"
1035           end
1036
1037           def hash_access_name(name, kind = :url)
1038             :"hash_for_#{name}_#{kind}"
1039           end
1040
1041           def define_named_route_methods(name, route)
1042             {:url => {}, :path => {:only_path => true}}.each do |kind, opts|
1043               hash = route.defaults.merge(:use_route => name).merge(opts)
1044               define_hash_access route, name, kind, hash
1045               define_url_helper route, name, kind, hash
1046             end
1047           end
1048          
1049           def define_hash_access(route, name, kind, options)
1050             selector = hash_access_name(name, kind)
1051             @module.send :module_eval, <<-end_eval # We use module_eval to avoid leaks
1052               def #{selector}(options = nil)
1053                 options ? #{options.inspect}.merge(options) : #{options.inspect}
1054               end
1055             end_eval
1056             @module.send(:protected, selector)
1057             helpers << selector
1058           end
1059          
1060           def define_url_helper(route, name, kind, options)
1061             selector = url_helper_name(name, kind)
1062            
1063             # The segment keys used for positional paramters
1064             segment_keys = route.segments.collect do |segment|
1065               segment.key if segment.respond_to? :key
1066             end.compact
1067             hash_access_method = hash_access_name(name, kind)
1068            
1069             @module.send :module_eval, <<-end_eval # We use module_eval to avoid leaks
1070               def #{selector}(*args)
1071                 opts = if args.empty? || Hash === args.first
1072                   args.first || {}
1073                 else
1074                   # allow ordered parameters to be associated with corresponding
1075                   # dynamic segments, so you can do
1076                   #
1077                   #   foo_url(bar, baz, bang)
1078                   #
1079                   # instead of
1080                   #
1081                   #   foo_url(:bar => bar, :baz => baz, :bang => bang)
1082                   args.zip(#{segment_keys.inspect}).inject({}) do |h, (v, k)|
1083                     h[k] = v
1084                     h
1085                   end
1086                 end
1087                
1088                 url_for(#{hash_access_method}(opts))
1089               end
1090             end_eval
1091             @module.send(:protected, selector)
1092             helpers << selector
1093           end
1094          
1095       end
1096  
1097       attr_accessor :routes, :named_routes
1098  
1099       def initialize
1100         self.routes = []
1101         self.named_routes = NamedRouteCollection.new
1102       end
1103
1104       # Subclasses and plugins may override this method to specify a different
1105       # RouteBuilder instance, so that other route DSL's can be created.
1106       def builder
1107         @builder ||= RouteBuilder.new
1108       end
1109
1110       def draw
1111         clear!
1112         yield Mapper.new(self)
1113         named_routes.install
1114       end
1115  
1116       def clear!
1117         routes.clear
1118         named_routes.clear
1119         @combined_regexp = nil
1120         @routes_by_controller = nil
1121       end
1122
1123       def empty?
1124         routes.empty?
1125       end
1126  
1127       def load!
1128         Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
1129         clear!
1130         load_routes!
1131         named_routes.install
1132       end
1133
1134       alias reload load!
1135
1136       def load_routes!
1137         if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes
1138           load File.join("#{RAILS_ROOT}/config/routes.rb")
1139         else
1140           add_route ":controller/:action/:id"
1141         end
1142       end
1143  
1144       def add_route(path, options = {})
1145         route = builder.build(path, options)
1146         routes << route
1147         route
1148       end
1149  
1150       def add_named_route(name, path, options = {})
1151         named_routes[name] = add_route(path, options)
1152       end
1153  
1154       def options_as_params(options)
1155         # If an explicit :controller was given, always make :action explicit
1156         # too, so that action expiry works as expected for things like
1157         #
1158         #   generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
1159         #
1160         # (the above is from the unit tests). In the above case, because the
1161         # controller was explicitly given, but no action, the action is implied to
1162         # be "index", not the recalled action of "show".
1163         #
1164         # great fun, eh?
1165
1166         options_as_params = options[:controller] ? { :action => "index" } : {}
1167         options.each do |k, value|
1168           options_as_params[k] = value.to_param
1169         end
1170         options_as_params
1171       end
1172  
1173       def build_expiry(options, recall)
1174         recall.inject({}) do |expiry, (key, recalled_value)|
1175           expiry[key] = (options.key?(key) && options[key] != recalled_value)
1176           expiry
1177         end
1178       end
1179
1180       # Generate the path indicated by the arguments, and return an array of
1181       # the keys that were not used to generate it.
1182       def extra_keys(options, recall={})
1183         generate_extras(options, recall).last
1184       end
1185
1186       def generate_extras(options, recall={})
1187         generate(options, recall, :generate_extras)
1188       end
1189
1190       def generate(options, recall = {}, method=:generate)
1191         named_route_name = options.delete(:use_route)
1192         if named_route_name
1193           named_route = named_routes[named_route_name]
1194           options = named_route.parameter_shell.merge(options)
1195         end
1196
1197         options = options_as_params(options)
1198         expire_on = build_expiry(options, recall)
1199
1200         # if the controller has changed, make sure it changes relative to the
1201         # current controller module, if any. In other words, if we're currently
1202         # on admin/get, and the new controller is 'set', the new controller
1203         # should really be admin/set.
1204         if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
1205           old_parts = recall[:controller].split('/')
1206           new_parts = options[:controller].split('/')
1207           parts = old_parts[0..-(new_parts.length + 1)] + new_parts
1208           options[:controller] = parts.join('/')
1209         end
1210
1211         # drop the leading '/' on the controller name
1212         options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/
1213         merged = recall.merge(options)
1214    
1215         if named_route
1216           path = named_route.generate(options, merged, expire_on)
1217           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?
1218           return path
1219         else
1220           merged[:action] ||= 'index'
1221           options[:action] ||= 'index'
1222  
1223           controller = merged[:controller]
1224           action = merged[:action]
1225
1226           raise RoutingError, "Need controller and action!" unless controller && action
1227           # don't use the recalled keys when determining which routes to check
1228           routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]
1229
1230           routes.each do |route|
1231             results = route.send(method, options, merged, expire_on)
1232             return results if results
1233           end
1234         end
1235    
1236         raise RoutingError, "No route matches #{options.inspect}"
1237       end
1238  
1239       def recognize(request)
1240         params = recognize_path(request.path, extract_request_environment(request))
1241         request.path_parameters = params.with_indifferent_access
1242         "#{params[:controller].camelize}Controller".constantize
1243       end
1244  
1245       def recognize_path(path, environment={})
1246         path = CGI.unescape(path)
1247         routes.each do |route|
1248           result = route.recognize(path, environment) and return result
1249         end
1250         raise RoutingError, "no route found to match #{path.inspect} with #{environment.inspect}"
1251       end
1252  
1253       def routes_by_controller
1254         @routes_by_controller ||= Hash.new do |controller_hash, controller|
1255           controller_hash[controller] = Hash.new do |action_hash, action|
1256             action_hash[action] = Hash.new do |key_hash, keys|
1257               key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys)
1258             end
1259           end
1260         end
1261       end
1262  
1263       def routes_for(options, merged, expire_on)
1264         raise "Need controller and action!" unless controller && action
1265         controller = merged[:controller]
1266         merged = options if expire_on[:controller]
1267         action = merged[:action] || 'index'
1268    
1269         routes_by_controller[controller][action][merged.keys]
1270       end
1271  
1272       def routes_for_controller_and_action(controller, action)
1273         selected = routes.select do |route|
1274           route.matches_controller_and_action? controller, action
1275         end
1276         (selected.length == routes.length) ? routes : selected
1277       end
1278  
1279       def routes_for_controller_and_action_and_keys(controller, action, keys)
1280         selected = routes.select do |route|
1281           route.matches_controller_and_action? controller, action
1282         end
1283         selected.sort_by do |route|
1284           (keys - route.significant_keys).length
1285         end
1286       end
1287
1288       # Subclasses and plugins may override this method to extract further attributes
1289       # from the request, for use by route conditions and such.
1290       def extract_request_environment(request)
1291         { :method => request.method }
1292       end
1293     end
1294
1295     Routes = RouteSet.new
1296   end
1297 end
1298
Note: See TracBrowser for help on using the browser.