aboutsummaryrefslogblamecommitdiffstats
path: root/actionpack/lib/action_controller/routing.rb
blob: d53becd2a94b1145b2fffb62edc8102b81beae10 (plain) (tree)
1
             














































                                                                
                       


                                
                 





                                 


















                                                                               




                                                                   
 
                                                           
                                                    
                                                                    
                                                             

                                                                

                                                                  



                                                            


                                     

                             

         



                                                                                                                   

                                                
         
 




                                                               










                                                         
         










                                                                                       
                                                                                                







                                                                             






                                                                                                                                                                     
                                                                             
 
                                                                                                                                                   

















                                                                                     
              
                                             
             
           



                                                                            
         
  
















                                                                                       
 






                                                                                     

         




                                                                        
           
                                                  
         









                                                                             
         




                                                                             

         



                                                        
 

                                                                            
                                                         
                              











                                                                                       
         
 





                                                                               
 


                               
                                   











































                                                                                             
 











                                                                                
           











                                                                                   
 
              




                                                                                                     






                                                                                  
           


           

       





                                           

         










                                                          

           






























                                                                                                                          
         
  
       
 


                                 
  


                                 
         


                                        
         











                                                                                          
            
                           


           

              
         

       
 





                                        
         


                              
         

       
 







                                                                               

         

                 

         
















                                                                                      
           


























                                                                                  
           
         


                                                     
         
                      
                                                                           
         








                                                                                  

                                                                                                 
                                                            





                                   

       



                                                                                           

         



                                                                              

         



                                                                                        
         
 


                                                                                  


         


                                      
                                                                               
         
 

                 
         


                                                                                           
         


                                                                                                                                                                                      
         




                        
                                     
                                

                                                      

                
       
 

                                                     
  


                                             
         
  

                                                               
         
  


















                                                                                 
 




















                                                                                
               
           
                                

         



                                                                                    



                                                                
 



                                                                                                   
           
    
                                            

         




                                                                                    
    

                                                                                       

           






                                                                                                                                  

             
    




                                                                                                         
           
 

                                          
         
  















                                                                                                                                                
           

         













                                                                                    
 





                                                                                    
         


                  
  














                                                                                




                                                   



                                                    

         
















































                                                                                 

             
 




























                                                                
                  












                                                                              

                   

                                                     
               


                                                 
             






                                                    
         
 




                                                                              
 










                                   

         







                            

         






                                                                                                                               

           



                                            
             
         


                                                     
         











                                                                                               
 




                                                                              
         
  



                                                                             
           
         
 














                                                                            
           







                                                                               
                                                                                                          


                                                                   
                                                
           














                                                                                                                   

                                                                                                     



                                                                    
             
           
    









                                                                                   
                                 










                                                                                                  



               






























                                                                                     

       
                         
     
   
require 'cgi'

class Object
  def to_param
    to_s
  end
end

class TrueClass
  def to_param
    self
  end
end

class FalseClass
  def to_param
    self
  end
end

class NilClass
  def to_param
    self
  end
end

class Regexp
  def number_of_captures
    Regexp.new("|#{source}").match('').captures.length
  end
  
  class << self
    def optionalize(pattern)
      case unoptionalize(pattern)
        when /\A(.|\(.*\))\Z/ then "#{pattern}?"
        else "(?:#{pattern})?"
      end
    end
    
    def unoptionalize(pattern)
      [/\A\(\?:(.*)\)\?\Z/, /\A(.|\(.*\))\?\Z/].each do |regexp|
        return $1 if regexp =~ pattern
      end
      return pattern
    end
  end
end

module ActionController
  module Routing
    SEPARATORS = %w( / ; . , ? )

    class << self
      def with_controllers(names)
        use_controllers! names
        yield
      ensure
        use_controllers! nil
      end

      def normalize_paths(paths=$LOAD_PATH)
        # do the hokey-pokey of path normalization...
        paths = paths.collect do |path|
          path = path.
            gsub("//", "/").           # replace double / chars with a single
            gsub("\\\\", "\\").        # replace double \ chars with a single
            gsub(%r{(.)[\\/]$}, '\1')  # drop final / or \ if path ends with it

          # eliminate .. paths where possible
          re = %r{\w+[/\\]\.\.[/\\]}
          path.gsub!(%r{\w+[/\\]\.\.[/\\]}, "") while path.match(re)
          path
        end

        # start with longest path, first
        paths = paths.uniq.sort_by { |path| - path.length }
      end

      def possible_controllers
        unless @possible_controllers
          @possible_controllers = []
        
          paths = $LOAD_PATH.select { |path| File.directory? path }

          seen_paths = Hash.new {|h, k| h[k] = true; false}
          normalize_paths(paths).each do |load_path|
            Dir["#{load_path}/**/*_controller.rb"].collect do |path|
              next if seen_paths[path.gsub(%r{^\.[/\\]}, "")]
            
              controller_name = path[(load_path.length + 1)..-1]
              next unless path_may_be_controller?(controller_name)

              controller_name.gsub!(/_controller\.rb\Z/, '')
              @possible_controllers << controller_name
            end
          end

          # remove duplicates
          @possible_controllers.uniq!
        end
        @possible_controllers
      end

      def path_may_be_controller?(path)
        path !~ /(?:rails\/.*\/(?:examples|test))|(?:actionpack\/lib\/action_controller.rb$)|(?:app\/controllers)/o
      end

      def use_controllers!(controller_names)
        @possible_controllers = controller_names
      end

      def controller_relative_to(controller, previous)
        if controller.nil?           then previous
        elsif controller[0] == ?/    then controller[1..-1]
        elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}"
        else controller
        end     
      end     
    end
  
    class Route
      attr_accessor :segments, :requirements, :conditions
  
      def initialize
        @segments = []
        @requirements = {}
        @conditions = {}
      end
  
      # Write and compile a +generate+ method for this Route.
      def write_generation
        # Build the main body of the generation
        body = "not_expired = true\n#{generation_extraction}\n#{generation_structure}"
    
        # If we have conditions that must be tested first, nest the body inside an if
        body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
        args = "options, hash, expire_on = {}"

        # Nest the body inside of a def block, and then compile it.
        method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
# puts "\n======================"
# puts
# p self
# puts
# puts method_decl
# puts
        instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"

        # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
        # are the same as the keys that were recalled from the previous request. Thus,
        # we can use the expire_on.keys to determine which keys ought to be used to build
        # the query string. (Never use keys from the recalled request when building the
        # query string.)

        method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(hash, expire_on))\nend"
        instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"

        method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(hash, expire_on)]\nend"
        instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
      end
  
      # Build several lines of code that extract values from the options hash. If any
      # of the values are missing or rejected then a return will be executed.
      def generation_extraction
        segments.collect do |segment|
          segment.extraction_code
        end.compact * "\n"
      end
  
      # Produce a condition expression that will check the requirements of this route
      # upon generation.
      def generation_requirements
        requirement_conditions = requirements.collect do |key, req|
          if req.is_a? Regexp
            value_regexp = Regexp.new "\\A#{req.source}\\Z"
            "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
          else
            "hash[:#{key}] == #{req.inspect}"
          end
        end
        requirement_conditions * ' && ' unless requirement_conditions.empty?
      end
      def generation_structure
        segments.last.string_structure segments[0..-2]
      end
  
      # Write and compile a +recognize+ method for this Route.
      def write_recognition
        # Create an if structure to extract the params from a match if it occurs.
        body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
        body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
    
        # Build the method declaration and compile it
        method_decl = "def recognize(path, env={})\n#{body}\nend"
# puts "\n======================"
# puts
# p self
# puts
# puts method_decl
# puts
        instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
        method_decl
      end

      # Plugins may override this method to add other conditions, like checks on
      # host, subdomain, and so forth. Note that changes here only affect route
      # recognition, not generation.
      def recognition_conditions
        result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
        result << "conditions[:method] === env[:method]" if conditions[:method]
        result
      end

      # Build the regular expression pattern that will match this route.
      def recognition_pattern(wrap = true)
        pattern = ''
        segments.reverse_each do |segment|
          pattern = segment.build_pattern pattern
        end
        wrap ? ("\\A" + pattern + "\\Z") : pattern
      end
  
      # Write the code to extract the parameters from a matched route.
      def recognition_extraction
        next_capture = 1
        extraction = segments.collect do |segment|
          x = segment.match_extraction next_capture
          next_capture += Regexp.new(segment.regexp_chunk).number_of_captures
          x
        end
        extraction.compact
      end
  
      # Write the real generation implementation and then resend the message.
      def generate(options, hash, expire_on = {})
        write_generation
        generate options, hash, expire_on
      end

      def generate_extras(options, hash, expire_on = {})
        write_generation
        generate_extras options, hash, expire_on
      end

      # Generate the query string with any extra keys in the hash and append
      # it to the given path, returning the new path.
      def append_query_string(path, hash, query_keys=nil)
        return nil unless path
        query_keys ||= extra_keys(hash)
        "#{path}#{build_query_string(hash, query_keys)}"
      end

      # Determine which keys in the given hash are "extra". Extra keys are
      # those that were not used to generate a particular route. The extra
      # keys also do not include those recalled from the prior request, nor
      # do they include any keys that were implied in the route (like a
      # :controller that is required, but not explicitly used in the text of
      # the route.)
      def extra_keys(hash, recall={})
        (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
      end

      # Build a query string from the keys of the given hash. If +only_keys+
      # is given (as an array), only the keys indicated will be used to build
      # the query string. The query string will correctly build array parameter
      # values.
      def build_query_string(hash, only_keys=nil)
        elements = []

        only_keys ||= hash.keys
        
        only_keys.each do |key|
          value = hash[key] or next
          key = CGI.escape key.to_s
          if value.class == Array
            key <<  '[]'
          else    
            value = [ value ] 
          end     
          value.each { |val| elements << "#{key}=#{CGI.escape(val.to_param.to_s)}" }
        end     
        
        query_string = "?#{elements.join("&")}" unless elements.empty?
        query_string || ""
      end
  
      # Write the real recognition implementation and then resend the message.
      def recognize(path, environment={})
        write_recognition
        recognize path, environment
      end
  
      # A route's parameter shell contains parameter values that are not in the
      # route's path, but should be placed in the recognized hash.
      # 
      # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route:
      # 
      #   map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
      # 
      def parameter_shell
        @parameter_shell ||= returning({}) do |shell|
          requirements.each do |key, requirement|
            shell[key] = requirement unless requirement.is_a? Regexp
          end
        end
      end
  
      # Return an array containing all the keys that are used in this route. This
      # includes keys that appear inside the path, and keys that have requirements
      # placed upon them.
      def significant_keys
        @significant_keys ||= returning [] do |sk|
          segments.each { |segment| sk << segment.key if segment.respond_to? :key }
          sk.concat requirements.keys
          sk.uniq!
        end
      end

      # Return a hash of key/value pairs representing the keys in the route that
      # have defaults, or which are specified by non-regexp requirements.
      def defaults
        @defaults ||= returning({}) do |hash|
          segments.each do |segment|
            next unless segment.respond_to? :default
            hash[segment.key] = segment.default unless segment.default.nil?
          end
          requirements.each do |key,req|
            next if Regexp === req || req.nil?
            hash[key] = req
          end
        end
      end
  
      def matches_controller_and_action?(controller, action)
        unless @matching_prepared
          @controller_requirement = requirement_for(:controller)
          @action_requirement = requirement_for(:action)
          @matching_prepared = true
        end

        (@controller_requirement.nil? || @controller_requirement === controller) &&
        (@action_requirement.nil? || @action_requirement === action)
      end

      def to_s
        @to_s ||= begin
            segs = segments.inject("") { |str,s| str << s.to_s }
            "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
          end     
      end     
  
    protected
  
      def requirement_for(key)
        return requirements[key] if requirements.key? key
        segments.each do |segment|
          return segment.regexp if segment.respond_to?(:key) && segment.key == key
        end
        nil
      end
  
    end

    class Segment
      attr_accessor :is_optional
      alias_method :optional?, :is_optional

      def initialize
        self.is_optional = false
      end

      def extraction_code
        nil
      end
  
      # Continue generating string for the prior segments.
      def continue_string_structure(prior_segments)
        if prior_segments.empty?
          interpolation_statement(prior_segments)
        else
          new_priors = prior_segments[0..-2]
          prior_segments.last.string_structure(new_priors)
        end
      end
  
      # Return a string interpolation statement for this segment and those before it.
      def interpolation_statement(prior_segments)
        chunks = prior_segments.collect { |s| s.interpolation_chunk }
        chunks << interpolation_chunk
        "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}"
      end
  
      def string_structure(prior_segments)
        optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments)
      end
  
      # Return an if condition that is true if all the prior segments can be generated.
      # If there are no optional segments before this one, then nil is returned.
      def all_optionals_available_condition(prior_segments)
        optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact
        optional_locals.empty? ? nil : " if #{optional_locals * ' && '}"
      end
  
      # Recognition
  
      def match_extraction(next_capture)
        nil
      end
  
      # Warning
  
      # Returns true if this segment is optional? because of a default. If so, then
      # no warning will be emitted regarding this segment.
      def optionality_implied?
        false
      end
  
    end

    class StaticSegment < Segment
      attr_accessor :value, :raw
      alias_method :raw?, :raw
  
      def initialize(value = nil)
        super()
        self.value = value
      end
  
      def interpolation_chunk
        raw? ? value : CGI.escape(value)
      end
  
      def regexp_chunk
        chunk = Regexp.escape value
        optional? ? Regexp.optionalize(chunk) : chunk
      end
  
      def build_pattern(pattern)
        escaped = Regexp.escape(value)
        if optional? && ! pattern.empty?
          "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})"
        elsif optional?
          Regexp.optionalize escaped
        else
          escaped + pattern
        end
      end
  
      def to_s
        value
      end
  
    end

    class DividerSegment < StaticSegment
  
      def initialize(value = nil)
        super(value)
        self.raw = true
        self.is_optional = true
      end
  
      def optionality_implied?
        true
      end
  
    end

    class DynamicSegment < Segment
      attr_accessor :key, :default, :regexp
  
      def initialize(key = nil, options = {})
        super()
        self.key = key
        self.default = options[:default] if options.key? :default
        self.is_optional = true if options[:optional] || options.key?(:default)
      end
  
      def to_s
        ":#{key}"
      end
  
      # The local variable name that the value of this segment will be extracted to.
      def local_name
        "#{key}_value"
      end
  
      def extract_value
        "#{local_name} = hash[:#{key}] #{"|| #{default.inspect}" if default}"
      end
      def value_check
        if default # Then we know it won't be nil
          "#{value_regexp.inspect} =~ #{local_name}" if regexp
        elsif optional?
          # If we have a regexp check that the value is not given, or that it matches.
          # If we have no regexp, return nil since we do not require a condition.
          "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp
        else # Then it must be present, and if we have a regexp, it must match too.
          "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}"
        end
      end
      def expiry_statement
        "not_expired, hash = false, options if not_expired && expire_on[:#{key}]"
      end
  
      def extraction_code
        s = extract_value
        vc = value_check
        s << "\nreturn [nil,nil] unless #{vc}" if vc
        s << "\n#{expiry_statement}"
      end
  
      def interpolation_chunk
        "\#{CGI.escape(#{local_name}.to_s)}"
      end
  
      def string_structure(prior_segments)
        if optional? # We have a conditional to do...
          # If we should not appear in the url, just write the code for the prior
          # segments. This occurs if our value is the default value, or, if we are
          # optional, if we have nil as our value.
          "if #{local_name} == #{default.inspect}\n" + 
            continue_string_structure(prior_segments) + 
          "\nelse\n" + # Otherwise, write the code up to here
            "#{interpolation_statement(prior_segments)}\nend"
        else
          interpolation_statement(prior_segments)
        end
      end
  
      def value_regexp
        Regexp.new "\\A#{regexp.source}\\Z" if regexp
      end
      def regexp_chunk
        regexp ? "(#{regexp.source})" : "([^#{Routing::SEPARATORS.join}]+)"
      end
  
      def build_pattern(pattern)
        chunk = regexp_chunk
        chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0
        pattern = "#{chunk}#{pattern}"
        optional? ? Regexp.optionalize(pattern) : pattern
      end
      def match_extraction(next_capture)
        hangon = (default ? "|| #{default.inspect}" : "if match[#{next_capture}]")
        
        # All non code-related keys (such as :id, :slug) have to be unescaped as other CGI params
        "params[:#{key}] = match[#{next_capture}] #{hangon}"
      end
  
      def optionality_implied?
        [:action, :id].include? key
      end
  
    end

    class ControllerSegment < DynamicSegment
      def regexp_chunk
        possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name }
        "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
      end

      # Don't CGI.escape the controller name, since it may have slashes in it,
      # like admin/foo.
      def interpolation_chunk
        "\#{#{local_name}.to_s}"
      end

      # Make sure controller names like Admin/Content are correctly normalized to
      # admin/content
      def extract_value
        "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase"
      end

      def match_extraction(next_capture)
        hangon = (default ? "|| #{default.inspect}" : "if match[#{next_capture}]")
        "params[:#{key}] = match[#{next_capture}].downcase #{hangon}"
      end
    end

    class PathSegment < DynamicSegment
      EscapedSlash = CGI.escape("/")
      def interpolation_chunk
        "\#{CGI.escape(#{local_name}.to_s).gsub(#{EscapedSlash.inspect}, '/')}"
      end

      def default
        ''
      end

      def default=(path)
        raise RoutingError, "paths cannot have non-empty default values" unless path.blank?
      end

      def match_extraction(next_capture)
        "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}"
      end

      def regexp_chunk
        regexp || "(.*)"
      end

      class Result < ::Array #:nodoc:
        def to_s() join '/' end 
        def self.new_escaped(strings)
          new strings.collect {|str| CGI.unescape str}
        end     
      end     
    end

    class RouteBuilder
      attr_accessor :separators, :optional_separators
  
      def initialize
        self.separators = Routing::SEPARATORS
        self.optional_separators = %w( / )
      end
  
      def separator_pattern(inverted = false)
        "[#{'^' if inverted}#{Regexp.escape(separators.join)}]"
      end
  
      def interval_regexp
        Regexp.new "(.*?)(#{separators.source}|$)"
      end
  
      # Accepts a "route path" (a string defining a route), and returns the array
      # of segments that corresponds to it. Note that the segment array is only
      # partially initialized--the defaults and requirements, for instance, need
      # to be set separately, via the #assign_route_options method, and the
      # #optional? method for each segment will not be reliable until after
      # #assign_route_options is called, as well.
      def segments_for_route_path(path)
        rest, segments = path, []
    
        until rest.empty?
          segment, rest = segment_for rest
          segments << segment
        end
        segments
      end

      # A factory method that returns a new segment instance appropriate for the
      # format of the given string.
      def segment_for(string)
        segment = case string
          when /\A:(\w+)/
            key = $1.to_sym
            case key
              when :action then DynamicSegment.new(key, :default => 'index')
              when :id then DynamicSegment.new(key, :optional => true)
              when :controller then ControllerSegment.new(key)
              else DynamicSegment.new key
            end
          when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true)
          when /\A\?(.*?)\?/
            returning segment = StaticSegment.new($1) do
              segment.is_optional = true
            end
          when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1)
          when Regexp.new(separator_pattern) then
            returning segment = DividerSegment.new($&) do
              segment.is_optional = (optional_separators.include? $&)
            end
        end
        [segment, $~.post_match]
      end
  
      # Split the given hash of options into requirement and default hashes. The
      # segments are passed alongside in order to distinguish between default values
      # and requirements.
      def divide_route_options(segments, options)
        options = options.dup
        requirements = (options.delete(:requirements) || {}).dup
        defaults     = (options.delete(:defaults)     || {}).dup
        conditions   = (options.delete(:conditions)   || {}).dup

        path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact
        options.each do |key, value|
          hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements
          hash[key] = value
        end
    
        [defaults, requirements, conditions]
      end
  
      # Takes a hash of defaults and a hash of requirements, and assigns them to
      # the segments. Any unused requirements (which do not correspond to a segment)
      # are returned as a hash.
      def assign_route_options(segments, defaults, requirements)
        route_requirements = {} # Requirements that do not belong to a segment
    
        segment_named = Proc.new do |key|
          segments.detect { |segment| segment.key == key if segment.respond_to?(:key) }
        end
    
        requirements.each do |key, requirement|
          segment = segment_named[key]
          if segment
            raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp)
            segment.regexp = requirement
          else
            route_requirements[key] = requirement
          end
        end
    
        defaults.each do |key, default|
          segment = segment_named[key]
          raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment
          segment.is_optional = true
          segment.default = default.to_param if default
        end

        ensure_required_segments(segments)
        route_requirements
      end
  
      # Makes sure that there are no optional segments that precede a required
      # segment. If any are found that precede a required segment, they are
      # made required.
      def ensure_required_segments(segments)
        allow_optional = true
        segments.reverse_each do |segment|
          allow_optional &&= segment.optional?
          if !allow_optional && segment.optional?
            unless segment.optionality_implied?
              warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required."
            end
            segment.is_optional = false
          elsif allow_optional & segment.respond_to?(:default) && segment.default
            # if a segment has a default, then it is optional
            segment.is_optional = true
          end
        end
      end

      # Construct and return a route with the given path and options.
      def build(path, options)
        # Wrap the path with slashes
        path = "/#{path}" unless path[0] == ?/
        path = "#{path}/" unless path[-1] == ?/
    
        segments = segments_for_route_path(path)
        defaults, requirements, conditions = divide_route_options(segments, options)
        requirements = assign_route_options(segments, defaults, requirements)

        route = Route.new
        route.segments = segments
        route.requirements = requirements
        route.conditions = conditions

        if !route.significant_keys.include?(:action) && !route.requirements[:action]
          route.requirements[:action] = "index"
          route.significant_keys << :action
        end

        route
      end
    end

    class RouteSet
  
      # Mapper instances are used to build routes. The object passed to the draw
      # block in config/routes.rb is a Mapper instance.
      # 
      # Mapper instances have relatively few instance methods, in order to avoid
      # clashes with named routes.
      class Mapper
        def initialize(set)
          @set = set
        end
    
        # Create an unnamed route with the provided +path+ and +options+. See 
        # SomeHelpfulUrl for an introduction to routes.
        def connect(path, options = {})
          @set.add_route(path, options)
        end

        def named_route(name, path, options = {})
          @set.add_named_route(name, path, options)
        end

        def method_missing(route_name, *args, &proc)
          super unless args.length >= 1 && proc.nil?
          @set.add_named_route(route_name, *args)
        end
      end

      # A NamedRouteCollection instance is a collection of named routes, and also
      # maintains an anonymous module that can be used to install helpers for the
      # named routes.
      class NamedRouteCollection
        include Enumerable

        attr_reader :routes, :helpers

        def initialize
          clear!
        end

        def clear!
          @routes = {}
          @helpers = []
          @module = Module.new
        end

        def add(name, route)
          routes[name.to_sym] = route
          define_hash_access_method(name, route)
          define_url_helper_method(name, route)
        end

        def get(name)
          routes[name.to_sym]
        end

        alias []=   add
        alias []    get
        alias clear clear!

        def each
          routes.each { |name, route| yield name, route }
          self
        end

        def names
          routes.keys
        end

        def length
          routes.length
        end

        def install(dest = ActionController::Base)
          dest.send :include, @module
          if dest.respond_to? :helper_method
            helpers.each { |name| dest.send :helper_method, name }
          end
        end

        private

          def url_helper_name(name)
            :"#{name}_url"
          end

          def hash_access_name(name)
            :"hash_for_#{name}_url"
          end

          def define_hash_access_method(name, route)
            method_name = hash_access_name(name)

            @module.send(:define_method, method_name) do |*args|
              hash = route.defaults.merge(:use_route => name)
              args.first ? hash.merge(args.first) : hash
            end

            @module.send(:protected, method_name)
            helpers << method_name
          end

          def define_url_helper_method(name, route)
            hash_access_method = hash_access_name(name)
            method_name = url_helper_name(name)

            @module.send(:define_method, method_name) do |*args|
              opts = if args.empty? || Hash === args.first
                args.first || {}
              else
                # allow ordered parameters to be associated with corresponding
                # dynamic segments, so you can do
                #
                #   foo_url(bar, baz, bang)
                #
                # instead of
                #
                #   foo_url(:bar => bar, :baz => baz, :bang => bang)
                route.segments.inject({}) do |opts, seg|
                  next opts unless seg.respond_to?(:key) && seg.key
                  opts[seg.key] = args.shift
                  break opts if args.empty?
                  opts
                end
              end

              url_for(send(hash_access_method, opts))
            end

            @module.send(:protected, method_name)
            helpers << method_name
          end
      end
  
      attr_accessor :routes, :named_routes
  
      def initialize
        self.routes = []
        self.named_routes = NamedRouteCollection.new
      end

      # Subclasses and plugins may override this method to specify a different
      # RouteBuilder instance, so that other route DSL's can be created.
      def builder
        @builder ||= RouteBuilder.new
      end

      def draw
        clear!
        yield Mapper.new(self)
        named_routes.install
      end
  
      def clear!
        routes.clear
        named_routes.clear
        @combined_regexp = nil
        @routes_by_controller = nil
      end

      def empty?
        routes.empty?
      end
  
      def load!
        clear!
        load_routes!
        named_routes.install
      end

      alias reload load!

      def load_routes!
        if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes
          load File.join("#{RAILS_ROOT}/config/routes.rb")
        else
          add_route ":controller/:action/:id"
        end
      end
  
      def add_route(path, options = {})
        route = builder.build(path, options)
        routes << route
        route
      end
  
      def add_named_route(name, path, options = {})
        named_routes[name] = add_route(path, options)
      end
  
      def options_as_params(options)
        # If an explicit :controller was given, always make :action explicit
        # too, so that action expiry works as expected for things like
        #
        #   generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
        #
        # (the above is from the unit tests). In the above case, because the
        # controller was explicitly given, but no action, the action is implied to
        # be "index", not the recalled action of "show".
        #
        # great fun, eh?

        options_as_params = options[:controller] ? { :action => "index" } : {}
        options.each do |k, value|
          options_as_params[k] = value.to_param
        end
        options_as_params
      end
  
      def build_expiry(options, recall)
        recall.inject({}) do |expiry, (key, recalled_value)|
          expiry[key] = (options.key?(key) && options[key] != recalled_value)
          expiry
        end
      end

      # Generate the path indicated by the arguments, and return an array of
      # the keys that were not used to generate it.
      def extra_keys(options, recall={})
        generate_extras(options, recall).last
      end

      def generate_extras(options, recall={})
        generate(options, recall, :generate_extras)
      end

      def generate(options, recall = {}, method=:generate)
        if options[:use_route]
          options = options.dup
          named_route = named_routes[options.delete(:use_route)]
          options = named_route.parameter_shell.merge(options)
        end

        options = options_as_params(options)
        expire_on = build_expiry(options, recall)

        # if the controller has changed, make sure it changes relative to the
        # current controller module, if any. In other words, if we're currently
        # on admin/get, and the new controller is 'set', the new controller
        # should really be admin/set.
        if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
          old_parts = recall[:controller].split('/')
          new_parts = options[:controller].split('/')
          parts = old_parts[0..-(new_parts.length + 1)] + new_parts
          options[:controller] = parts.join('/')
        end

        # drop the leading '/' on the controller name
        options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/
        merged = recall.merge(options)
    
        if named_route
          return named_route.generate(options, merged, expire_on)
        else
          merged[:action] ||= 'index'
          options[:action] ||= 'index'
  
          controller = merged[:controller]
          action = merged[:action]

          raise "Need controller and action!" unless controller && action
          # don't use the recalled keys when determining which routes to check
          routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]

          routes.each do |route|
            results = route.send(method, options, merged, expire_on)
            return results if results
          end
        end
    
        raise RoutingError, "No route matches #{options.inspect}"
      end
  
      def recognize(request)
        params = recognize_path(request.path, extract_request_environment(request))
        request.path_parameters = params.with_indifferent_access
        "#{params[:controller].camelize}Controller".constantize
      end
  
      def recognize_path(path, environment={})
        path = CGI.unescape(path)
        routes.each do |route|
          result = route.recognize(path, environment) and return result
        end
        raise RoutingError, "no route found to match #{path.inspect} with #{environment.inspect}"
      end
  
      def routes_by_controller
        @routes_by_controller ||= Hash.new do |controller_hash, controller|
          controller_hash[controller] = Hash.new do |action_hash, action|
            action_hash[action] = Hash.new do |key_hash, keys|
              key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys)
            end
          end
        end
      end
  
      def routes_for(options, merged, expire_on)
        raise "Need controller and action!" unless controller && action
        controller = merged[:controller]
        merged = options if expire_on[:controller]
        action = merged[:action] || 'index'
    
        routes_by_controller[controller][action][merged.keys]
      end
  
      def routes_for_controller_and_action(controller, action)
        selected = routes.select do |route|
          route.matches_controller_and_action? controller, action
        end
        (selected.length == routes.length) ? routes : selected
      end
  
      def routes_for_controller_and_action_and_keys(controller, action, keys)
        selected = routes.select do |route|
          route.matches_controller_and_action? controller, action
        end
        selected.sort_by do |route|
          (keys - route.significant_keys).length
        end
      end

      # Subclasses and plugins may override this method to extract further attributes
      # from the request, for use by route conditions and such.
      def extract_request_environment(request)
        { :method => request.method }
      end
    end

    Routes = RouteSet.new
  end
end