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

Ticket #10835: routing-speedup.patch

File routing-speedup.patch, 95.2 kB (added by oleganza, 9 months ago)

patch speeds routes recognition up 2.4-2.7 times

  • a/actionpack/Rakefile

    old new  
    3333  t.verbose = true 
    3434} 
    3535 
     36desc "Run routes-related tests only" 
     37Rake::TestTask.new(:test_routes) do |t| 
     38  t.libs << "test" 
     39  t.test_files = Dir.glob("test/c*/**/routing_test.rb") + 
     40                 Dir.glob("test/c*/**/resources_test.rb") + 
     41                 Dir.glob("test/c*/**/polymorphic_routes_test.rb") 
     42  t.verbose = true 
     43end 
     44 
     45desc "Run routing tests only, no resources" 
     46Rake::TestTask.new(:test_routing) do |t| 
     47  t.libs << "test" 
     48  t.test_files=Dir.glob("test/c*/**/routing_test.rb") 
     49  t.verbose = true 
     50end 
     51 
    3652desc 'ActiveRecord Integration Tests' 
    3753Rake::TestTask.new(:test_active_record_integration) do |t| 
    3854  t.libs << "test" 
  • a/actionpack/lib/action_controller/routing.rb

    old new  
    22require 'uri' 
    33require 'action_controller/polymorphic_routes' 
    44require 'action_controller/routing_optimisation' 
     5require 'action_controller/routing/route' 
     6require 'action_controller/routing/segments' 
     7require 'action_controller/routing/route_builder' 
     8require 'action_controller/routing/route_set' 
    59 
    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 
     10require 'action_controller/routing/goodies' 
    5111 
    5212module ActionController 
    5313  # == Routing 
     
    352312      end 
    353313    end 
    354314 
    355     class Route #:nodoc: 
    356       attr_accessor :segments, :requirements, :conditions, :optimise 
    357  
    358       def initialize 
    359         @segments = [] 
    360         @requirements = {} 
    361         @conditions = {} 
    362         @optimise = true 
    363       end 
    364  
    365       # Indicates whether the routes should be optimised with the string interpolation 
    366       # version of the named routes methods. 
    367       def optimise? 
    368         @optimise && ActionController::Base::optimise_named_routes 
    369       end 
    370  
    371       def segment_keys 
    372         segments.collect do |segment| 
    373           segment.key if segment.respond_to? :key 
    374         end.compact 
    375       end 
    376  
    377       # Write and compile a +generate+ method for this Route. 
    378       def write_generation 
    379         # Build the main body of the generation 
    380         body = "expired = false\n#{generation_extraction}\n#{generation_structure}" 
    381  
    382         # If we have conditions that must be tested first, nest the body inside an if 
    383         body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements 
    384         args = "options, hash, expire_on = {}" 
    385  
    386         # Nest the body inside of a def block, and then compile it. 
    387         raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend" 
    388         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" 
    389  
    390         # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash 
    391         # are the same as the keys that were recalled from the previous request. Thus, 
    392         # we can use the expire_on.keys to determine which keys ought to be used to build 
    393         # the query string. (Never use keys from the recalled request when building the 
    394         # query string.) 
    395  
    396         method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend" 
    397         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" 
    398  
    399         method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend" 
    400         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" 
    401         raw_method 
    402       end 
    403  
    404       # Build several lines of code that extract values from the options hash. If any 
    405       # of the values are missing or rejected then a return will be executed. 
    406       def generation_extraction 
    407         segments.collect do |segment| 
    408           segment.extraction_code 
    409         end.compact * "\n" 
    410       end 
    411  
    412       # Produce a condition expression that will check the requirements of this route 
    413       # upon generation. 
    414       def generation_requirements 
    415         requirement_conditions = requirements.collect do |key, req| 
    416           if req.is_a? Regexp 
    417             value_regexp = Regexp.new "\\A#{req.source}\\Z" 
    418             "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]" 
    419           else 
    420             "hash[:#{key}] == #{req.inspect}" 
    421           end 
    422         end 
    423         requirement_conditions * ' && ' unless requirement_conditions.empty? 
    424       end 
    425  
    426       def generation_structure 
    427         segments.last.string_structure segments[0..-2] 
    428       end 
    429  
    430       # Write and compile a +recognize+ method for this Route. 
    431       def write_recognition 
    432         # Create an if structure to extract the params from a match if it occurs. 
    433         body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams" 
    434         body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend" 
    435  
    436         # Build the method declaration and compile it 
    437         method_decl = "def recognize(path, env={})\n#{body}\nend" 
    438         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" 
    439         method_decl 
    440       end 
    441  
    442       # Plugins may override this method to add other conditions, like checks on 
    443       # host, subdomain, and so forth. Note that changes here only affect route 
    444       # recognition, not generation. 
    445       def recognition_conditions 
    446         result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"] 
    447         result << "conditions[:method] === env[:method]" if conditions[:method] 
    448         result 
    449       end 
    450  
    451       # Build the regular expression pattern that will match this route. 
    452       def recognition_pattern(wrap = true) 
    453         pattern = '' 
    454         segments.reverse_each do |segment| 
    455           pattern = segment.build_pattern pattern 
    456         end 
    457         wrap ? ("\\A" + pattern + "\\Z") : pattern 
    458       end 
    459  
    460       # Write the code to extract the parameters from a matched route. 
    461       def recognition_extraction 
    462         next_capture = 1 
    463         extraction = segments.collect do |segment| 
    464           x = segment.match_extraction(next_capture) 
    465           next_capture += Regexp.new(segment.regexp_chunk).number_of_captures 
    466           x 
    467         end 
    468         extraction.compact 
    469       end 
    470  
    471       # Write the real generation implementation and then resend the message. 
    472       def generate(options, hash, expire_on = {}) 
    473         write_generation 
    474         generate options, hash, expire_on 
    475       end 
    476  
    477       def generate_extras(options, hash, expire_on = {}) 
    478         write_generation 
    479         generate_extras options, hash, expire_on 
    480       end 
    481  
    482       # Generate the query string with any extra keys in the hash and append 
    483       # it to the given path, returning the new path. 
    484       def append_query_string(path, hash, query_keys=nil) 
    485         return nil unless path 
    486         query_keys ||= extra_keys(hash) 
    487         "#{path}#{build_query_string(hash, query_keys)}" 
    488       end 
    489  
    490       # Determine which keys in the given hash are "extra". Extra keys are 
    491       # those that were not used to generate a particular route. The extra 
    492       # keys also do not include those recalled from the prior request, nor 
    493       # do they include any keys that were implied in the route (like a 
    494       # :controller that is required, but not explicitly used in the text of 
    495       # the route.) 
    496       def extra_keys(hash, recall={}) 
    497         (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys 
    498       end 
    499  
    500       # Build a query string from the keys of the given hash. If +only_keys+ 
    501       # is given (as an array), only the keys indicated will be used to build 
    502       # the query string. The query string will correctly build array parameter 
    503       # values. 
    504       def build_query_string(hash, only_keys = nil) 
    505         elements = [] 
    506  
    507         (only_keys || hash.keys).each do |key| 
    508           if value = hash[key] 
    509             elements << value.to_query(key) 
    510           end 
    511         end 
    512  
    513         elements.empty? ? '' : "?#{elements.sort * '&'}" 
    514       end 
    515  
    516       # Write the real recognition implementation and then resend the message. 
    517       def recognize(path, environment={}) 
    518         write_recognition 
    519         recognize path, environment 
    520       end 
    521  
    522       # A route's parameter shell contains parameter values that are not in the 
    523       # route's path, but should be placed in the recognized hash. 
    524       # 
    525       # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route: 
    526       # 
    527       #   map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ 
    528       # 
    529       def parameter_shell 
    530         @parameter_shell ||= returning({}) do |shell| 
    531           requirements.each do |key, requirement| 
    532             shell[key] = requirement unless requirement.is_a? Regexp 
    533           end 
    534         end 
    535       end 
    536  
    537       # Return an array containing all the keys that are used in this route. This 
    538       # includes keys that appear inside the path, and keys that have requirements 
    539       # placed upon them. 
    540       def significant_keys 
    541         @significant_keys ||= returning [] do |sk| 
    542           segments.each { |segment| sk << segment.key if segment.respond_to? :key } 
    543           sk.concat requirements.keys 
    544           sk.uniq! 
    545         end 
    546       end 
    547  
    548       # Return a hash of key/value pairs representing the keys in the route that 
    549       # have defaults, or which are specified by non-regexp requirements. 
    550       def defaults 
    551         @defaults ||= returning({}) do |hash| 
    552           segments.each do |segment| 
    553             next unless segment.respond_to? :default 
    554             hash[segment.key] = segment.default unless segment.default.nil? 
    555           end 
    556           requirements.each do |key,req| 
    557             next if Regexp === req || req.nil? 
    558             hash[key] = req 
    559           end 
    560         end 
    561       end 
    562  
    563       def matches_controller_and_action?(controller, action) 
    564         unless defined? @matching_prepared 
    565           @controller_requirement = requirement_for(:controller) 
    566           @action_requirement = requirement_for(:action) 
    567           @matching_prepared = true 
    568         end 
    569  
    570         (@controller_requirement.nil? || @controller_requirement === controller) && 
    571         (@action_requirement.nil? || @action_requirement === action) 
    572       end 
    573  
    574       def to_s 
    575         @to_s ||= begin 
    576           segs = segments.inject("") { |str,s| str << s.to_s } 
    577           "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect] 
    578         end 
    579       end 
    580  
    581     protected 
    582       def requirement_for(key) 
    583         return requirements[key] if requirements.key? key 
    584         segments.each do |segment| 
    585           return segment.regexp if segment.respond_to?(:key) && segment.key == key 
    586         end 
    587         nil 
    588       end 
    589  
    590     end 
    591  
    592     class Segment #:nodoc: 
    593       RESERVED_PCHAR = ':@&=+$,;' 
    594       UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze 
    595  
    596       attr_accessor :is_optional 
    597       alias_method :optional?, :is_optional 
    598  
    599       def initialize 
    600         self.is_optional = false 
    601       end 
    602  
    603       def extraction_code 
    604         nil 
    605       end 
    606  
    607       # Continue generating string for the prior segments. 
    608       def continue_string_structure(prior_segments) 
    609         if prior_segments.empty? 
    610           interpolation_statement(prior_segments) 
    611         else 
    612           new_priors = prior_segments[0..-2] 
    613           prior_segments.last.string_structure(new_priors) 
    614         end 
    615       end 
    616  
    617       def interpolation_chunk 
    618         URI.escape(value, UNSAFE_PCHAR) 
    619       end 
    620  
    621       # Return a string interpolation statement for this segment and those before it. 
    622       def interpolation_statement(prior_segments) 
    623         chunks = prior_segments.collect { |s| s.interpolation_chunk } 
    624         chunks << interpolation_chunk 
    625         "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}" 
    626       end 
    627  
    628       def string_structure(prior_segments) 
    629         optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments) 
    630       end 
    631  
    632       # Return an if condition that is true if all the prior segments can be generated. 
    633       # If there are no optional segments before this one, then nil is returned. 
    634       def all_optionals_available_condition(prior_segments) 
    635         optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact 
    636         optional_locals.empty? ? nil : " if #{optional_locals * ' && '}" 
    637       end 
    638  
    639       # Recognition 
    640  
    641       def match_extraction(next_capture) 
    642         nil 
    643       end 
    644  
    645       # Warning 
    646  
    647       # Returns true if this segment is optional? because of a default. If so, then 
    648       # no warning will be emitted regarding this segment. 
    649       def optionality_implied? 
    650         false 
    651       end 
    652     end 
    653  
    654     class StaticSegment < Segment #:nodoc: 
    655       attr_accessor :value, :raw 
    656       alias_method :raw?, :raw 
    657  
    658       def initialize(value = nil) 
    659         super() 
    660         self.value = value 
    661       end 
    662  
    663       def interpolation_chunk 
    664         raw? ? value : super 
    665       end 
    666  
    667       def regexp_chunk 
    668         chunk = Regexp.escape(value) 
    669         optional? ? Regexp.optionalize(chunk) : chunk 
    670       end 
    671  
    672       def build_pattern(pattern) 
    673         escaped = Regexp.escape(value) 
    674         if optional? && ! pattern.empty? 
    675           "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})" 
    676         elsif optional? 
    677           Regexp.optionalize escaped 
    678         else 
    679           escaped + pattern 
    680         end 
    681       end 
    682  
    683       def to_s 
    684         value 
    685       end 
    686     end 
    687  
    688     class DividerSegment < StaticSegment #:nodoc: 
    689       def initialize(value = nil) 
    690         super(value) 
    691         self.raw = true 
    692         self.is_optional = true 
    693       end 
    694  
    695       def optionality_implied? 
    696         true 
    697       end 
    698     end 
    699  
    700     class DynamicSegment < Segment #:nodoc: 
    701       attr_accessor :key, :default, :regexp 
    702  
    703       def initialize(key = nil, options = {}) 
    704         super() 
    705         self.key = key 
    706         self.default = options[:default] if options.key? :default 
    707         self.is_optional = true if options[:optional] || options.key?(:default) 
    708       end 
    709  
    710       def to_s 
    711         ":#{key}" 
    712       end 
    713  
    714       # The local variable name that the value of this segment will be extracted to. 
    715       def local_name 
    716         "#{key}_value" 
    717       end 
    718  
    719       def extract_value 
    720         "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}" 
    721       end 
    722       def value_check 
    723         if default # Then we know it won't be nil 
    724           "#{value_regexp.inspect} =~ #{local_name}" if regexp 
    725         elsif optional? 
    726           # If we have a regexp check that the value is not given, or that it matches. 
    727           # If we have no regexp, return nil since we do not require a condition. 
    728           "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp 
    729         else # Then it must be present, and if we have a regexp, it must match too. 
    730           "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}" 
    731         end 
    732       end 
    733       def expiry_statement 
    734         "expired, hash = true, options if !expired && expire_on[:#{key}]" 
    735       end 
    736  
    737       def extraction_code 
    738         s = extract_value 
    739         vc = value_check 
    740         s << "\nreturn [nil,nil] unless #{vc}" if vc 
    741         s << "\n#{expiry_statement}" 
    742       end 
    743  
    744       def interpolation_chunk(value_code = "#{local_name}") 
    745         "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}" 
    746       end 
    747  
    748       def string_structure(prior_segments) 
    749         if optional? # We have a conditional to do... 
    750           # If we should not appear in the url, just write the code for the prior 
    751           # segments. This occurs if our value is the default value, or, if we are 
    752           # optional, if we have nil as our value. 
    753           "if #{local_name} == #{default.inspect}\n" + 
    754             continue_string_structure(prior_segments) + 
    755           "\nelse\n" + # Otherwise, write the code up to here 
    756             "#{interpolation_statement(prior_segments)}\nend" 
    757         else 
    758           interpolation_statement(prior_segments) 
    759         end 
    760       end 
    761  
    762       def value_regexp 
    763         Regexp.new "\\A#{regexp.source}\\Z" if regexp 
    764       end 
    765       def regexp_chunk 
    766         regexp ? "(#{regexp.source})" : "([^#{Routing::SEPARATORS.join}]+)" 
    767       end 
    768  
    769       def build_pattern(pattern) 
    770         chunk = regexp_chunk 
    771         chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0 
    772         pattern = "#{chunk}#{pattern}" 
    773         optional? ? Regexp.optionalize(pattern) : pattern 
    774       end 
    775       def match_extraction(next_capture) 
    776         # All non code-related keys (such as :id, :slug) are URI-unescaped as 
    777         # path parameters. 
    778         default_value = default ? default.inspect : nil 
    779         %[ 
    780           value = if (m = match[#{next_capture}]) 
    781             URI.unescape(m) 
    782           else 
    783             #{default_value} 
    784           end 
    785           params[:#{key}] = value if value 
    786         ] 
    787       end 
    788  
    789       def optionality_implied? 
    790         [:action, :id].include? key 
    791       end 
    792  
    793     end 
    794  
    795     class ControllerSegment < DynamicSegment #:nodoc: 
    796       def regexp_chunk 
    797         possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name } 
    798         "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))" 
    799       end 
    800  
    801       # Don't URI.escape the controller name since it may contain slashes. 
    802       def interpolation_chunk(value_code = "#{local_name}") 
    803         "\#{#{value_code}.to_s}" 
    804       end 
    805  
    806       # Make sure controller names like Admin/Content are correctly normalized to 
    807       # admin/content 
    808       def extract_value 
    809         "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase" 
    810       end 
    811  
    812       def match_extraction(next_capture) 
    813         if default 
    814           "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'" 
    815         else 
    816           "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]" 
    817         end 
    818       end 
    819     end 
    820  
    821     class PathSegment < DynamicSegment #:nodoc: 
    822       RESERVED_PCHAR = "#{Segment::RESERVED_PCHAR}/" 
    823       UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze 
    824  
    825       def interpolation_chunk(value_code = "#{local_name}") 
    826         "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::PathSegment::UNSAFE_PCHAR)}" 
    827       end 
    828  
    829       def default 
    830         '' 
    831       end 
    832  
    833       def default=(path) 
    834         raise RoutingError, "paths cannot have non-empty default values" unless path.blank? 
    835       end 
    836  
    837       def match_extraction(next_capture) 
    838         "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}" 
    839       end 
    840  
    841       def regexp_chunk 
    842         regexp || "(.*)" 
    843       end 
    844  
    845       def optionality_implied? 
    846         true 
    847       end 
    848  
    849       class Result < ::Array #:nodoc: 
    850         def to_s() join '/' end 
    851         def self.new_escaped(strings) 
    852           new strings.collect {|str| URI.unescape str} 
    853         end 
    854       end 
    855     end 
    856  
    857     class RouteBuilder #:nodoc: 
    858       attr_accessor :separators, :optional_separators 
    859  
    860       def initialize 
    861         self.separators = Routing::SEPARATORS 
    862         self.optional_separators = %w( / ) 
    863       end 
    864  
    865       def separator_pattern(inverted = false) 
    866         "[#{'^' if inverted}#{Regexp.escape(separators.join)}]" 
    867       end 
    868  
    869       def interval_regexp 
    870         Regexp.new "(.*?)(#{separators.source}|$)" 
    871       end 
    872  
    873       # Accepts a "route path" (a string defining a route), and returns the array 
    874       # of segments that corresponds to it. Note that the segment array is only 
    875       # partially initialized--the defaults and requirements, for instance, need 
    876       # to be set separately, via the #assign_route_options method, and the 
    877       # #optional? method for each segment will not be reliable until after 
    878       # #assign_route_options is called, as well. 
    879       def segments_for_route_path(path) 
    880         rest, segments = path, [] 
    881  
    882         until rest.empty? 
    883           segment, rest = segment_for rest 
    884           segments << segment 
    885         end 
    886         segments 
    887       end 
    888  
    889       # A factory method that returns a new segment instance appropriate for the 
    890       # format of the given string. 
    891       def segment_for(string) 
    892         segment = case string 
    893           when /\A:(\w+)/ 
    894             key = $1.to_sym 
    895             case key 
    896               when :controller then ControllerSegment.new(key) 
    897               else DynamicSegment.new key 
    898             end 
    899           when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true) 
    900           when /\A\?(.*?)\?/ 
    901             returning segment = StaticSegment.new($1) do 
    902               segment.is_optional = true 
    903             end 
    904           when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1) 
    905           when Regexp.new(separator_pattern) then 
    906             returning segment = DividerSegment.new($&) do 
    907               segment.is_optional = (optional_separators.include? $&) 
    908             end 
    909         end 
    910         [segment, $~.post_match] 
    911       end 
    912  
    913       # Split the given hash of options into requirement and default hashes. The 
    914       # segments are passed alongside in order to distinguish between default values 
    915       # and requirements. 
    916       def divide_route_options(segments, options) 
    917         options = options.dup 
    918  
    919         if options[:namespace] 
    920           options[:controller] = "#{options[:path_prefix]}/#{options[:controller]}" 
    921           options.delete(:path_prefix) 
    922           options.delete(:name_prefix) 
    923           options.delete(:namespace) 
    924         end 
    925  
    926         requirements = (options.delete(:requirements) || {}).dup 
    927         defaults     = (options.delete(:defaults)     || {}).dup 
    928         conditions   = (options.delete(:conditions)   || {}).dup 
    929  
    930         path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact 
    931         options.each do |key, value| 
    932           hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements 
    933           hash[key] = value 
    934         end 
    935  
    936         [defaults, requirements, conditions] 
    937       end 
    938  
    939       # Takes a hash of defaults and a hash of requirements, and assigns them to 
    940       # the segments. Any unused requirements (which do not correspond to a segment) 
    941       # are returned as a hash. 
    942       def assign_route_options(segments, defaults, requirements) 
    943         route_requirements = {} # Requirements that do not belong to a segment 
    944  
    945         segment_named = Proc.new do |key| 
    946           segments.detect { |segment| segment.key == key if segment.respond_to?(:key) } 
    947         end 
    948  
    949         requirements.each do |key, requirement| 
    950           segment = segment_named[key] 
    951           if segment 
    952             raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp) 
    953             if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} 
    954               raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" 
    955             end 
    956             segment.regexp = requirement 
    957           else 
    958             route_requirements[key] = requirement 
    959           end 
    960         end 
    961  
    962         defaults.each do |key, default| 
    963           segment = segment_named[key] 
    964           raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment 
    965           segment.is_optional = true 
    966           segment.default = default.to_param if default 
    967         end 
    968  
    969         assign_default_route_options(segments) 
    970         ensure_required_segments(segments) 
    971         route_requirements 
    972       end 
    973  
    974       # Assign default options, such as 'index' as a default for :action. This 
    975       # method must be run *after* user supplied requirements and defaults have 
    976       # been applied to the segments. 
    977       def assign_default_route_options(segments) 
    978         segments.each do |segment| 
    979           next unless segment.is_a? DynamicSegment 
    980           case segment.key 
    981             when :action 
    982               if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index' 
    983                 segment.default ||= 'index' 
    984                 segment.is_optional = true 
    985               end 
    986             when :id 
    987               if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ '' 
    988                 segment.is_optional = true 
    989               end 
    990           end 
    991         end 
    992       end 
    993  
    994       # Makes sure that there are no optional segments that precede a required 
    995       # segment. If any are found that precede a required segment, they are 
    996       # made required. 
    997       def ensure_required_segments(segments) 
    998         allow_optional = true 
    999         segments.reverse_each do |segment| 
    1000           allow_optional &&= segment.optional? 
    1001           if !allow_optional && segment.optional? 
    1002             unless segment.optionality_implied? 
    1003               warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required." 
    1004             end 
    1005             segment.is_optional = false 
    1006           elsif allow_optional && segment.respond_to?(:default) && segment.default 
    1007             # if a segment has a default, then it is optional 
    1008             segment.is_optional = true 
    1009           end 
    1010         end 
    1011       end 
    1012  
    1013       # Construct and return a route with the given path and options. 
    1014       def build(path, options) 
    1015         # Wrap the path with slashes 
    1016         path = "/#{path}" unless path[0] == ?/ 
    1017         path = "#{path}/" unless path[-1] == ?/ 
    1018  
    1019         path = "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options[:path_prefix] 
    1020  
    1021         segments = segments_for_route_path(path) 
    1022         defaults, requirements, conditions = divide_route_options(segments, options) 
    1023         requirements = assign_route_options(segments, defaults, requirements) 
    1024  
    1025         route = Route.new 
    1026  
    1027         route.segments = segments 
    1028         route.requirements = requirements 
    1029         route.conditions = conditions 
    1030  
    1031         if !route.significant_keys.include?(:action) && !route.requirements[:action] 
    1032           route.requirements[:action] = "index" 
    1033           route.significant_keys << :action 
    1034         end 
    1035  
    1036         # Routes cannot use the current string interpolation method 
    1037         # if there are user-supplied :requirements as the interpolation 
    1038         # code won't raise RoutingErrors when generating 
    1039         if options.key?(:requirements) || route.requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION 
    1040           route.optimise = false 
    1041         end 
    1042  
    1043         if !route.significant_keys.include?(:controller) 
    1044           raise ArgumentError, "Illegal route: the :controller must be specified!" 
    1045         end 
    1046  
    1047         route 
    1048       end 
    1049     end 
    1050  
    1051     class RouteSet #:nodoc:  
    1052       # Mapper instances are used to build routes. The object passed to the draw 
    1053       # block in config/routes.rb is a Mapper instance. 
    1054       # 
    1055       # Mapper instances have relatively few instance methods, in order to avoid 
    1056       # clashes with named routes. 
    1057       class Mapper #:doc: 
    1058         def initialize(set) #:nodoc: 
    1059           @set = set 
    1060         end 
    1061  
    1062         # Create an unnamed route with the provided +path+ and +options+. See 
    1063         # ActionController::Routing for an introduction to routes. 
    1064         def connect(path, options = {}) 
    1065           @set.add_route(path, options) 
    1066         end 
    1067  
    1068         # Creates a named route called "root" for matching the root level request. 
    1069         def root(options = {}) 
    1070           named_route("root", '', options) 
    1071         end 
    1072  
    1073         def named_route(name, path, options = {}) #:nodoc: 
    1074           @set.add_named_route(name, path, options) 
    1075         end 
    1076  
    1077         # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. 
    1078         # Example: 
    1079         # 
    1080         #   map.namespace(:admin) do |admin| 
    1081         #     admin.resources :products, 
    1082         #       :has_many => [ :tags, :images, :variants ] 
    1083         #   end 
    1084         # 
    1085         # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. 
    1086         # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for 
    1087         # Admin::TagsController. 
    1088         def namespace(name, options = {}, &block) 
    1089           if options[:namespace] 
    1090             with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) 
    1091           else 
    1092             with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) 
    1093           end 
    1094         end 
    1095  
    1096         def method_missing(route_name, *args, &proc) #:nodoc: 
    1097           super unless args.length >= 1 && proc.nil? 
    1098           @set.add_named_route(route_name, *args) 
    1099         end 
    1100       end 
    1101  
    1102       # A NamedRouteCollection instance is a collection of named routes, and also 
    1103       # maintains an anonymous module that can be used to install helpers for the 
    1104       # named routes. 
    1105       class NamedRouteCollection #:nodoc: 
    1106         include Enumerable 
    1107         include ActionController::Routing::Optimisation 
    1108         attr_reader :routes, :helpers 
    1109  
    1110         def initialize 
    1111           clear! 
    1112         end 
    1113  
    1114         def clear! 
    1115           @routes = {} 
    1116           @helpers = [] 
    1117  
    1118           @module ||= Module.new 
    1119           @module.instance_methods.each do |selector| 
    1120             @module.class_eval { remove_method selector } 
    1121           end 
    1122         end 
    1123  
    1124         def add(name, route) 
    1125           routes[name.to_sym] = route 
    1126           define_named_route_methods(name, route) 
    1127         end 
    1128  
    1129         def get(name) 
    1130           routes[name.to_sym] 
    1131         end 
    1132  
    1133         alias []=   add 
    1134         alias []    get 
    1135         alias clear clear! 
    1136  
    1137         def each 
    1138           routes.each { |name, route| yield name, route } 
    1139           self 
    1140         end 
    1141  
    1142         def names 
    1143           routes.keys 
    1144         end 
    1145  
    1146         def length 
    1147           routes.length 
    1148         end 
    1149  
    1150         def reset! 
    1151           old_routes = routes.dup 
    1152           clear! 
    1153           old_routes.each do |name, route| 
    1154             add(name, route) 
    1155           end 
    1156         end 
    1157  
    1158         def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false) 
    1159           reset! if regenerate 
    1160           Array(destinations).each do |dest| 
    1161             dest.send! :include, @module 
    1162           end 
    1163         end 
    1164  
    1165         private 
    1166           def url_helper_name(name, kind = :url) 
    1167             :"#{name}_#{kind}" 
    1168           end 
    1169  
    1170           def hash_access_name(name, kind = :url) 
    1171             :"hash_for_#{name}_#{kind}" 
    1172           end 
    1173  
    1174           def define_named_route_methods(name, route) 
    1175             {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts| 
    1176               hash = route.defaults.merge(:use_route => name).merge(opts) 
    1177               define_hash_access route, name, kind, hash 
    1178               define_url_helper route, name, kind, hash 
    1179             end 
    1180           end 
    1181  
    1182           def define_hash_access(route, name, kind, options) 
    1183             selector = hash_access_name(name, kind) 
    1184             @module.module_eval <<-end_eval # We use module_eval to avoid leaks 
    1185               def #{selector}(options = nil) 
    1186                 options ? #{options.inspect}.merge(options) : #{options.inspect} 
    1187               end 
    1188               protected :#{selector} 
    1189             end_eval 
    1190             helpers << selector 
    1191           end 
    1192  
    1193           def define_url_helper(route, name, kind, options) 
    1194             selector = url_helper_name(name, kind) 
    1195             # The segment keys used for positional paramters 
    1196  
    1197             hash_access_method = hash_access_name(name, kind) 
    1198  
    1199             # allow ordered parameters to be associated with corresponding 
    1200             # dynamic segments, so you can do 
    1201             # 
    1202             #   foo_url(bar, baz, bang) 
    1203             # 
    1204             # instead of 
    1205             # 
    1206             #   foo_url(:bar => bar, :baz => baz, :bang => bang) 
    1207             # 
    1208             # Also allow options hash, so you can do 
    1209             # 
    1210             #   foo_url(bar, baz, bang, :sort_by => 'baz') 
    1211             # 
    1212             @module.module_eval <<-end_eval # We use module_eval to avoid leaks 
    1213               def #{selector}(*args) 
    1214                 #{generate_optimisation_block(route, kind)} 
    1215  
    1216                 opts = if args.empty? || Hash === args.first 
    1217                   args.first || {} 
    1218                 else 
    1219                   options = args.extract_options! 
    1220                   args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| 
    1221                     h[k] = v 
    1222                     h 
    1223                   end 
    1224                   options.merge(args) 
    1225                 end 
    1226  
    1227                 url_for(#{hash_access_method}(opts)) 
    1228               end 
    1229               protected :#{selector} 
    1230             end_eval 
    1231             helpers << selector 
    1232           end 
    1233       end 
    1234  
    1235       attr_accessor :routes, :named_routes 
    1236  
    1237       def initialize 
    1238         self.routes = [] 
    1239         self.named_routes = NamedRouteCollection.new 
    1240       end 
    1241  
    1242       # Subclasses and plugins may override this method to specify a different 
    1243       # RouteBuilder instance, so that other route DSL's can be created. 
    1244       def builder 
    1245         @builder ||= RouteBuilder.new 
    1246       end 
    1247  
    1248       def draw 
    1249         clear! 
    1250         yield Mapper.new(self) 
    1251         install_helpers 
    1252       end 
    1253  
    1254       def clear! 
    1255         routes.clear 
    1256         named_routes.clear 
    1257         @combined_regexp = nil 
    1258         @routes_by_controller = nil 
    1259       end 
    1260  
    1261       def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) 
    1262         Array(destinations).each { |d| d.module_eval { include Helpers } } 
    1263         named_routes.install(destinations, regenerate_code) 
    1264       end 
    1265  
    1266       def empty? 
    1267         routes.empty? 
    1268       end 
    1269  
    1270       def load! 
    1271         Routing.use_controllers! nil # Clear the controller cache so we may discover new ones 
    1272         clear! 
    1273         load_routes! 
    1274         install_helpers 
    1275       end 
    1276  
    1277       # reload! will always force a reload whereas load checks the timestamp first 
    1278       alias reload! load! 
    1279  
    1280       def reload 
    1281         if @routes_last_modified && defined?(RAILS_ROOT) 
    1282           mtime = File.stat("#{RAILS_ROOT}/config/routes.rb").mtime 
    1283           # if it hasn't been changed, then just return 
    1284           return if mtime == @routes_last_modified 
    1285           # if it has changed then record the new time and fall to the load! below 
    1286           @routes_last_modified = mtime 
    1287         end 
    1288         load! 
    1289       end 
    1290  
    1291       def load_routes! 
    1292         if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes 
    1293           load File.join("#{RAILS_ROOT}/config/routes.rb") 
    1294           @routes_last_modified = File.stat("#{RAILS_ROOT}/config/routes.rb").mtime 
    1295         else 
    1296           add_route ":controller/:action/:id" 
    1297         end 
    1298       end 
    1299  
    1300       def add_route(path, options = {}) 
    1301         route = builder.build(path, options) 
    1302         routes << route 
    1303         route 
    1304       end 
    1305  
    1306       def add_named_route(name, path, options = {}) 
    1307         # TODO - is options EVER used? 
    1308         name = options[:name_prefix] + name.to_s if options[:name_prefix] 
    1309         named_routes[name.to_sym] = add_route(path, options) 
    1310       end 
    1311  
    1312       def options_as_params(options) 
    1313         # If an explicit :controller was given, always make :action explicit 
    1314         # too, so that action expiry works as expected for things like 
    1315         # 
    1316         #   generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) 
    1317         # 
    1318         # (the above is from the unit tests). In the above case, because the 
    1319         # controller was explicitly given, but no action, the action is implied to 
    1320         # be "index", not the recalled action of "show". 
    1321         # 
    1322         # great fun, eh? 
    1323  
    1324         options_as_params = options.clone 
    1325         options_as_params[:action] ||= 'index' if options[:controller] 
    1326         options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action] 
    1327         options_as_params 
    1328       end 
    1329  
    1330       def build_expiry(options, recall) 
    1331         recall.inject({}) do |expiry, (key, recalled_value)| 
    1332           expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param) 
    1333           expiry 
    1334         end 
    1335       end 
    1336  
    1337       # Generate the path indicated by the arguments, and return an array of 
    1338       # the keys that were not used to generate it. 
    1339       def extra_keys(options, recall={}) 
    1340         generate_extras(options, recall).last 
    1341       end 
    1342  
    1343       def generate_extras(options, recall={}) 
    1344         generate(options, recall, :generate_extras) 
    1345       end 
    1346  
    1347       def generate(options, recall = {}, method=:generate) 
    1348         named_route_name = options.delete(:use_route) 
    1349         generate_all = options.delete(:generate_all) 
    1350         if named_route_name 
    1351           named_route = named_routes[named_route_name] 
    1352           options = named_route.parameter_shell.merge(options) 
    1353         end 
    1354  
    1355         options = options_as_params(options) 
    1356         expire_on = build_expiry(options, recall) 
    1357  
    1358         if options[:controller] 
    1359           options[:controller] = options[:controller].to_s 
    1360         end 
    1361         # if the controller has changed, make sure it changes relative to the 
    1362         # current controller module, if any. In other words, if we're currently 
    1363         # on admin/get, and the new controller is 'set', the new controller 
    1364         # should really be admin/set. 
    1365         if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/ 
    1366           old_parts = recall[:controller].split('/') 
    1367           new_parts = options[:controller].split('/') 
    1368           parts = old_parts[0..-(new_parts.length + 1)] + new_parts 
    1369           options[:controller] = parts.join('/') 
    1370         end 
    1371  
    1372         # drop the leading '/' on the controller name 
    1373         options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/ 
    1374         merged = recall.merge(options) 
    1375  
    1376         if named_route 
    1377           path = named_route.generate(options, merged, expire_on) 
    1378           if path.nil? 
    1379             raise_named_route_error(options, named_route, named_route_name) 
    1380           else 
    1381             return path 
    1382           end 
    1383         else 
    1384           merged[:action] ||= 'index' 
    1385           options[:action] ||= 'index' 
    1386  
    1387           controller = merged[:controller] 
    1388           action = merged[:action] 
    1389  
    1390           raise RoutingError, "Need controller and action!" unless controller && action 
    1391  
    1392         &