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

Ticket #9573: routing.rb

File routing.rb, 50.0 kB (added by pkcahier, 1 year ago)
Line 
1 require 'cgi'
2 require 'uri'
3 require 'action_controller/polymorphic_routes'
4 require 'action_controller/routing_optimisation'
5
6 class Object
7   def to_param
8     to_s
9   end
10 end
11
12 class TrueClass
13   def to_param
14     self
15   end
16 end
17
18 class FalseClass
19   def to_param
20     self
21   end
22 end
23
24 class NilClass
25   def to_param
26     self
27   end
28 end
29
30 class Regexp #:nodoc:
31   def number_of_captures
32     Regexp.new("|#{source}").match('').captures.length
33   end
34  
35   class << self
36     def optionalize(pattern)
37       case unoptionalize(pattern)
38         when /\A(.|\(.*\))\Z/ then "#{pattern}?"
39         else "(?:#{pattern})?"
40       end
41     end
42    
43     def unoptionalize(pattern)
44       [/\A\(\?:(.*)\)\?\Z/, /\A(.|\(.*\))\?\Z/].each do |regexp|
45         return $1 if regexp =~ pattern
46       end
47       return pattern
48     end
49   end
50 end
51
52 module ActionController
53   # == Routing
54   #
55   # The routing module provides URL rewriting in native Ruby. It's a way to
56   # redirect incoming requests to controllers and actions. This replaces
57   # mod_rewrite rules. Best of all Rails' Routing works with any web server.
58   # Routes are defined in routes.rb in your RAILS_ROOT/config directory.
59   #
60   # Consider the following route, installed by Rails when you generate your
61   # application:
62   #
63   #   map.connect ':controller/:action/:id'
64   #
65   # This route states that it expects requests to consist of a
66   # :controller followed by an :action that in turns is fed by some :id
67   #
68   # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end up
69   # with:
70   #
71   #   params = { :controller => 'blog',
72   #              :action     => 'edit'
73   #              :id         => '22'
74   #           }
75   #
76   # Think of creating routes as drawing a map for your requests. The map tells
77   # them where to go based on some predefined pattern:
78   #
79   #  ActionController::Routing::Routes.draw do |map|
80   #   Pattern 1 tells some request to go to one place
81   #   Pattern 2 tell them to go to another
82   #   ...
83   #  end
84   #
85   # The following symbols are special:
86   #
87   #   :controller maps to your controller name
88   #   :action     maps to an action with your controllers
89   #   
90   # Other names simply map to a parameter as in the case of +:id+.
91   #   
92   # == Route priority
93   #
94   # Not all routes are created equally. Routes have priority defined by the
95   # order of appearance of the routes in the routes.rb file. The priority goes
96   # from top to bottom. The last route in that file is at the lowest priority
97   # will be applied last. If no route matches, 404 is returned.
98   #
99   # Within blocks, the empty pattern goes first i.e. is at the highest priority.
100   # In practice this works out nicely:
101   #
102   #  ActionController::Routing::Routes.draw do |map|
103   #    map.with_options :controller => 'blog' do |blog|
104   #      blog.show    '',  :action => 'list'
105   #    end
106   #    map.connect ':controller/:action/:view
107   #  end
108   #
109   # In this case, invoking blog controller (with an URL like '/blog/')
110   # without parameters will activate the 'list' action by default.
111   #
112   # == Defaults routes and default parameters
113   #
114   # Setting a default route is straightforward in Rails because by appending a
115   # Hash to the end of your mapping you can set default parameters.
116   #
117   # Example:
118   #  ActionController::Routing:Routes.draw do |map|
119   #    map.connect ':controller/:action/:id', :controller => 'blog'
120   #  end
121   #
122   # This sets up  +blog+ as the default controller if no other is specified.
123   # This means visiting '/' would invoke the blog controller.
124   #
125   # More formally, you can define defaults in a route with the +:defaults+ key.
126   #   
127   #   map.connect ':controller/:id/:action', :action => 'show', :defaults => { :page => 'Dashboard' }
128   #
129   # == Named routes
130   #
131   # Routes can be named with the syntax <tt>map.name_of_route options</tt>,
132   # allowing for easy reference within your source as +name_of_route_url+
133   # for the full URL and +name_of_route_path+ for the URI path.
134   #
135   # Example:
136   #   # In routes.rb
137   #   map.login 'login', :controller => 'accounts', :action => 'login'
138   #
139   #   # With render, redirect_to, tests, etc.
140   #   redirect_to login_url
141   #
142   # Arguments can be passed as well.
143   #
144   #   redirect_to show_item_path(:id => 25)
145   #
146   # Use <tt>map.root</tt> as a shorthand to name a route for the root path ""
147   #
148   #   # In routes.rb
149   #   map.root :controller => 'blogs'
150   #
151   #   # would recognize http://www.example.com/ as
152   #   params = { :controller => 'blogs', :action => 'index' }
153   #
154   #   # and provide these named routes
155   #   root_url   # => 'http://www.example.com/'
156   #   root_path  # => ''
157   #
158   # Note: when using +with_options+, the route is simply named after the
159   # method you call on the block parameter rather than map.
160   #
161   #   # In routes.rb
162   #   map.with_options :controller => 'blog' do |blog|
163   #     blog.show    '',            :action  => 'list'
164   #     blog.delete  'delete/:id',  :action  => 'delete',
165   #     blog.edit    'edit/:id',    :action  => 'edit'
166   #   end
167   #
168   #   # provides named routes for show, delete, and edit
169   #   link_to @article.title, show_path(:id => @article.id)
170   #
171   # == Pretty URLs
172   #
173   # Routes can generate pretty URLs. For example:
174   #
175   #  map.connect 'articles/:year/:month/:day',
176   #              :controller => 'articles',
177   #              :action     => 'find_by_date',
178   #              :year       => /\d{4}/,
179   #              :month => /\d{1,2}/,
180   #              :day   => /\d{1,2}/
181  
182   #  # Using the route above, the url below maps to:
183   #  # params = {:year => '2005', :month => '11', :day => '06'}
184   #  # http://localhost:3000/articles/2005/11/06
185   #
186   # == Regular Expressions and parameters
187   # You can specify a reqular expression to define a format for a parameter.
188   #
189   #  map.geocode 'geocode/:postalcode', :controller => 'geocode',
190   #              :action => 'show', :postalcode => /\d{5}(-\d{4})?/
191   #
192   # or, more formally:
193   #
194   #   map.geocode 'geocode/:postalcode', :controller => 'geocode',
195   #               :action => 'show', :requirements => { :postalcode => /\d{5}(-\d{4})?/ }
196   #
197   # == Route globbing
198   #
199   # Specifying <tt>*[string]</tt> as part of a rule like :
200   #
201   #  map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?'
202   #
203   # 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. 
204   #
205   # == Reloading routes
206   #
207   # You can reload routes if you feel you must:
208   #
209   #  ActionController::Routing::Routes.reload
210   #
211   # This will clear all named routes and reload routes.rb
212   #
213   # == Testing Routes
214   #
215   # The two main methods for testing your routes:
216   #
217   # === +assert_routing+
218   #
219   #  def test_movie_route_properly_splits
220   #   opts = {:controller => "plugin", :action => "checkout", :id => "2"}
221   #   assert_routing "plugin/checkout/2", opts
222   #  end
223  
224   # +assert_routing+ lets you test whether or not the route properly resolves into options.
225   #
226   # === +assert_recognizes+
227   #
228   #  def test_route_has_options
229   #   opts = {:controller => "plugin", :action => "show", :id => "12"}
230   #   assert_recognizes opts, "/plugins/show/12"
231   #  end
232   #
233   # Note the subtle difference between the two: +assert_routing+ tests that
234   # an URL fits options while +assert_recognizes+ tests that an URL
235   # breaks into parameters properly.
236   #
237   # In tests you can simply pass the URL or named route to +get+ or +post+.
238   #
239   #  def send_to_jail
240   #    get '/jail'
241   #    assert_response :success
242   #    assert_template "jail/front"
243   #  end
244   #
245   #  def goes_to_login
246   #    get login_url
247   #    #...
248   #  end
249   #
250   module Routing
251     SEPARATORS = %w( / ; . , ? )
252
253     HTTP_METHODS = [:get, :head, :post, :put, :delete]
254
255     # The root paths which may contain controller files
256     mattr_accessor :controller_paths
257     self.controller_paths = []
258    
259     # Indicates whether or not optimise the generated named
260     # route helper methods
261     mattr_accessor :optimise_named_routes
262     self.optimise_named_routes = true
263    
264     # A helper module to hold URL related helpers.
265     module Helpers
266       include PolymorphicRoutes
267     end
268    
269     class << self
270       def with_controllers(names)
271         prior_controllers = @possible_controllers
272         use_controllers! names
273         yield
274       ensure
275         use_controllers! prior_controllers
276       end
277
278       def normalize_paths(paths)
279         # do the hokey-pokey of path normalization...
280         paths = paths.collect do |path|
281           path = path.
282             gsub("//", "/").           # replace double / chars with a single
283             gsub("\\\\", "\\").        # replace double \ chars with a single
284             gsub(%r{(.)[\\/]$}, '\1')  # drop final / or \ if path ends with it
285
286           # eliminate .. paths where possible
287           re = %r{\w+[/\\]\.\.[/\\]}
288           path.gsub!(%r{\w+[/\\]\.\.[/\\]}, "") while path.match(re)
289           path
290         end
291
292         # start with longest path, first
293         paths = paths.uniq.sort_by { |path| - path.length }
294       end
295
296       def possible_controllers
297         unless @possible_controllers
298           @possible_controllers = []
299        
300           paths = controller_paths.select { |path| File.directory?(path) && path != "." }
301
302           seen_paths = Hash.new {|h, k| h[k] = true; false}
303           normalize_paths(paths).each do |load_path|
304             Dir["#{load_path}/**/*_controller.rb"].sort.collect do |path|
305               next if seen_paths[path.gsub(%r{^\.[/\\]}, "")]
306              
307               controller_name = path[(load_path.length + 1)..-1]
308              
309               controller_name.gsub!(/_controller\.rb\Z/, '')
310               @possible_controllers << controller_name
311             end
312           end
313
314           # remove duplicates
315           @possible_controllers.uniq!
316         end
317         @possible_controllers
318       end
319
320       def use_controllers!(controller_names)
321         @possible_controllers = controller_names
322       end
323
324       def controller_relative_to(controller, previous)
325         if controller.nil?           then previous
326         elsif controller[0] == ?/    then controller[1..-1]
327         elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}"
328         else controller
329         end     
330       end     
331     end
332  
333     class Route #:nodoc:
334       attr_accessor :segments, :requirements, :conditions, :optimise
335      
336       def initialize
337         @segments = []
338         @requirements = {}
339         @conditions = {}
340       end
341      
342       # Indicates whether the routes should be optimised with the string interpolation
343       # version of the named routes methods.
344       def optimise?
345         @optimise
346       end
347      
348       def segment_keys
349         segments.collect do |segment|
350           segment.key if segment.respond_to? :key
351         end.compact
352       end
353  
354       # Write and compile a +generate+ method for this Route.
355       def write_generation
356         # Build the main body of the generation
357         body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
358    
359         # If we have conditions that must be tested first, nest the body inside an if
360         body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
361         args = "options, hash, expire_on = {}"
362
363         # Nest the body inside of a def block, and then compile it.
364         raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
365         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
366
367         # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
368         # are the same as the keys that were recalled from the previous request. Thus,
369         # we can use the expire_on.keys to determine which keys ought to be used to build
370         # the query string. (Never use keys from the recalled request when building the
371         # query string.)
372
373         method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
374         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
375
376         method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
377         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
378         raw_method
379       end
380  
381       # Build several lines of code that extract values from the options hash. If any
382       # of the values are missing or rejected then a return will be executed.
383       def generation_extraction
384         segments.collect do |segment|
385           segment.extraction_code
386         end.compact * "\n"
387       end
388  
389       # Produce a condition expression that will check the requirements of this route
390       # upon generation.
391       def generation_requirements
392         requirement_conditions = requirements.collect do |key, req|
393           if req.is_a? Regexp
394             value_regexp = Regexp.new "\\A#{req.source}\\Z"
395             "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
396           else
397             "hash[:#{key}] == #{req.inspect}"
398           end
399         end
400         requirement_conditions * ' && ' unless requirement_conditions.empty?
401       end
402      
403       def generation_structure
404         segments.last.string_structure segments[0..-2]
405       end
406  
407       # Write and compile a +recognize+ method for this Route.
408       def write_recognition
409         # Create an if structure to extract the params from a match if it occurs.
410         body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
411         body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
412    
413         # Build the method declaration and compile it
414         method_decl = "def recognize(path, env={})\n#{body}\nend"
415         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
416         method_decl
417       end
418
419       # Plugins may override this method to add other conditions, like checks on
420       # host, subdomain, and so forth. Note that changes here only affect route
421       # recognition, not generation.
422       def recognition_conditions
423         result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
424         result << "conditions[:method] === env[:method]" if conditions[:method]
425         result
426       end
427
428       # Build the regular expression pattern that will match this route.
429       def recognition_pattern(wrap = true)
430         pattern = ''
431         segments.reverse_each do |segment|
432           pattern = segment.build_pattern pattern
433         end
434         wrap ? ("\\A" + pattern + "\\Z") : pattern
435       end
436      
437       # Write the code to extract the parameters from a matched route.
438       def recognition_extraction
439         next_capture = 1
440         extraction = segments.collect do |segment|
441           x = segment.match_extraction(next_capture)
442           next_capture += Regexp.new(segment.regexp_chunk).number_of_captures
443           x
444         end
445         extraction.compact
446       end
447  
448       # Write the real generation implementation and then resend the message.
449       def generate(options, hash, expire_on = {})
450         write_generation
451         generate options, hash, expire_on
452       end
453
454       def generate_extras(options, hash, expire_on = {})
455         write_generation
456         generate_extras options, hash, expire_on
457       end
458
459       # Generate the query string with any extra keys in the hash and append
460       # it to the given path, returning the new path.
461       def append_query_string(path, hash, query_keys=nil)
462         return nil unless path
463         query_keys ||= extra_keys(hash)
464         "#{path}#{build_query_string(hash, query_keys)}"
465       end
466
467       # Determine which keys in the given hash are "extra". Extra keys are
468       # those that were not used to generate a particular route. The extra
469       # keys also do not include those recalled from the prior request, nor
470       # do they include any keys that were implied in the route (like a
471       # :controller that is required, but not explicitly used in the text of
472       # the route.)
473       def extra_keys(hash, recall={})
474         (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
475       end
476
477       # Build a query string from the keys of the given hash. If +only_keys+
478       # is given (as an array), only the keys indicated will be used to build
479       # the query string. The query string will correctly build array parameter
480       # values.
481       def build_query_string(hash, only_keys = nil)
482         elements = []
483
484         (only_keys || hash.keys).each do |key|
485           if value = hash[key]
486             elements << value.to_query(key)
487           end
488         end
489
490         elements.empty? ? '' : "?#{elements.sort * '&'}"
491       end
492
493       # Write the real recognition implementation and then resend the message.
494       def recognize(path, environment={})
495         write_recognition
496         recognize path, environment
497       end
498  
499       # A route's parameter shell contains parameter values that are not in the
500       # route's path, but should be placed in the recognized hash.
501       #
502       # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route:
503       #
504       #   map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
505       #
506       def parameter_shell
507         @parameter_shell ||= returning({}) do |shell|
508           requirements.each do |key, requirement|
509             shell[key] = requirement unless requirement.is_a? Regexp
510           end
511         end
512       end
513  
514       # Return an array containing all the keys that are used in this route. This
515       # includes keys that appear inside the path, and keys that have requirements
516       # placed upon them.
517       def significant_keys
518         @significant_keys ||= returning [] do |sk|
519           segments.each { |segment| sk << segment.key if segment.respond_to? :key }
520           sk.concat requirements.keys
521           sk.uniq!
522         end
523       end
524
525       # Return a hash of key/value pairs representing the keys in the route that
526       # have defaults, or which are specified by non-regexp requirements.
527       def defaults
528         @defaults ||= returning({}) do |hash|
529           segments.each do |segment|
530             next unless segment.respond_to? :default
531             hash[segment.key] = segment.default unless segment.default.nil?
532           end
533           requirements.each do |key,req|
534             next if Regexp === req || req.nil?
535             hash[key] = req
536           end
537         end
538       end
539
540       def matches_controller_and_action?(controller, action)
541         unless defined? @matching_prepared
542           @controller_requirement = requirement_for(:controller)
543           @action_requirement = requirement_for(:action)
544           @matching_prepared = true
545         end
546
547         (@controller_requirement.nil? || @controller_requirement === controller) &&
548         (@action_requirement.nil? || @action_requirement === action)
549       end
550
551       def to_s
552         @to_s ||= begin
553           segs = segments.inject("") { |str,s| str << s.to_s }
554           "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
555         end
556       end
557  
558     protected
559       def requirement_for(key)
560         return requirements[key] if requirements.key? key
561         segments.each do |segment|
562           return segment.regexp if segment.respond_to?(:key) && segment.key == key
563         end
564         nil
565       end
566
567     end
568
569     class Segment #:nodoc:
570       RESERVED_PCHAR = ':@&=+$'
571       UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
572
573       attr_accessor :is_optional
574       alias_method :optional?, :is_optional
575
576       def initialize
577         self.is_optional = false
578       end
579
580       def extraction_code
581         nil
582       end
583
584       # Continue generating string for the prior segments.
585       def continue_string_structure(prior_segments)
586         if prior_segments.empty?
587           interpolation_statement(prior_segments)
588         else
589           new_priors = prior_segments[0..-2]
590           prior_segments.last.string_structure(new_priors)
591         end
592       end
593
594       def interpolation_chunk
595         URI.escape(value, UNSAFE_PCHAR)
596       end
597
598       # Return a string interpolation statement for this segment and those before it.
599       def interpolation_statement(prior_segments)
600         chunks = prior_segments.collect { |s| s.interpolation_chunk }
601         chunks << interpolation_chunk
602         "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}"
603       end
604  
605       def string_structure(prior_segments)
606         optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments)
607       end
608  
609       # Return an if condition that is true if all the prior segments can be generated.
610       # If there are no optional segments before this one, then nil is returned.
611       def all_optionals_available_condition(prior_segments)
612         optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact
613         optional_locals.empty? ? nil : " if #{optional_locals * ' && '}"
614       end
615  
616       # Recognition
617  
618       def match_extraction(next_capture)
619         nil
620       end
621  
622       # Warning
623  
624       # Returns true if this segment is optional? because of a default. If so, then
625       # no warning will be emitted regarding this segment.
626       def optionality_implied?
627         false
628       end
629     end
630
631     class StaticSegment < Segment #:nodoc:
632       attr_accessor :value, :raw
633       alias_method :raw?, :raw
634  
635       def initialize(value = nil)
636         super()
637         self.value = value
638       end
639  
640       def interpolation_chunk
641         raw? ? value : super
642       end
643  
644       def regexp_chunk
645         chunk = Regexp.escape(value)
646         optional? ? Regexp.optionalize(chunk) : chunk
647       end
648  
649       def build_pattern(pattern)
650         escaped = Regexp.escape(value)
651         if optional? && ! pattern.empty?
652           "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})"
653         elsif optional?
654           Regexp.optionalize escaped
655         else
656           escaped + pattern
657         end
658       end
659  
660       def to_s
661         value
662