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

Changeset 1496

Show
Ignore:
Timestamp:
06/24/05 16:40:01 (3 years ago)
Author:
david
Message:

Improved performance of Routes generation by a factor of 5 #1434 [Nicholas Seckar] Added named routes (NEEDS BETTER DESCRIPTION) #1434 [Nicholas Seckar]

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/actionpack/CHANGELOG

    r1494 r1496  
    11*SVN* 
    22 
    3 * Improved AbstractRequest documentation.  #1483 [court3nay@gmail.com] 
    4  
    5 * Added ActionController::Base.allow_concurrency to control whether the application is thread-safe, so multi-threaded servers like WEBrick knows whether to apply a mutex around the performance of each action. Action Pack and Active Record are by default thread-safe, but many applications may not be. Turned off by default. 
     3* Improved performance of Routes generation by a factor of 5 #1434 [Nicholas Seckar]  
     4 
     5* Added named routes (NEEDS BETTER DESCRIPTION) #1434 [Nicholas Seckar] 
     6 
     7* Improved AbstractRequest documentation #1483 [court3nay@gmail.com] 
     8 
     9* Added ActionController::Base.allow_concurrency to control whether the application is thread-safe, so multi-threaded servers like WEBrick knows whether to apply a mutex around the performance of each action. Turned off by default. EXPERIMENTAL FEATURE. 
    610 
    711* Added TextHelper#word_wrap(text, line_length = 80) #1449 [tuxie@dekadance.se] 
  • trunk/actionpack/lib/action_controller/base.rb

    r1487 r1496  
    22require 'action_controller/response' 
    33require 'action_controller/routing' 
     4require 'action_controller/code_generation' 
    45require 'action_controller/url_rewriter' 
    56require 'drb' 
     
    1213  class MissingTemplate < ActionControllerError #:nodoc: 
    1314  end 
    14   class RoutingError < ActionControllerError#:nodoc: 
     15  class RoutingError < ActionControllerError #:nodoc: 
    1516    attr_reader :failures 
    1617    def initialize(message, failures=[]) 
  • trunk/actionpack/lib/action_controller/request.rb

    r1494 r1496  
    3838      method == :head 
    3939    end 
    40  
    4140 
    4241    # Determine whether the body of a POST request is URL-encoded (default), 
     
    7978    end 
    8079 
    81     # Is the X-Requested-With HTTP header present and does it contain the 
    82     # string "XMLHttpRequest"?.  The Prototype Javascript library sends this 
    83     # header with every Ajax request. 
     80    # Returns true if the request's "X-Requested-With" header contains 
     81    # "XMLHttpRequest". (The Prototype Javascript library sends this header with 
     82    # every Ajax request.) 
    8483    def xml_http_request? 
    8584      not /XMLHttpRequest/i.match(env['HTTP_X_REQUESTED_WITH']).nil? 
     
    187186    def path_parameters=(parameters) 
    188187      @path_parameters = parameters 
    189       @parameters = nil 
     188      @symbolized_path_parameters = @parameters = nil 
     189    end 
     190     
     191    def symbolized_path_parameters 
     192      @symbolized_path_parameters ||= path_parameters.symbolize_keys 
    190193    end 
    191194 
  • trunk/actionpack/lib/action_controller/routing.rb

    r1158 r1496  
    11module ActionController 
    2   # See http://manuals.rubyonrails.com/read/chapter/65 
    32  module Routing 
    4     class Route #:nodoc: 
    5       attr_reader :defaults # The defaults hash 
    6        
    7       def initialize(path, hash={}) 
    8         raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash) 
    9         @defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {} 
    10         @requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {} 
    11         self.items = path 
     3    class << self 
     4 
     5      def expiry_hash(options, recall) 
     6        k = v = nil 
     7        expire_on = {} 
     8        options.each {|k, v| expire_on[k] = ((rcv = recall[k]) && (rcv != v))} 
     9        expire_on 
     10      end 
     11 
     12      def extract_parameter_value(parameter) #:nodoc: 
     13        CGI.escape((parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s)  
     14      end 
     15      def controller_relative_to(controller, previous) 
     16        if controller.nil?           then previous 
     17        elsif controller[0] == ?/    then controller[1..-1] 
     18        elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}" 
     19        else controller 
     20        end 
     21      end 
     22 
     23      def treat_hash(hash) 
     24        k = v = nil 
    1225        hash.each do |k, v| 
    13           raise TypeError, "Hash keys must be symbols!" unless k.kind_of? Symbol 
    14           if v.kind_of? Regexp 
    15             raise ArgumentError, "Regexp requirement on #{k}, but #{k} is not in this route's path!" unless @items.include? k 
    16             @requirements[k] = v 
     26          hash[k] = (v.respond_to? :to_param) ? v.to_param.to_s : v.to_s  
     27        end 
     28        hash 
     29      end 
     30    end 
     31 
     32    class RoutingError < StandardError 
     33    end 
     34 
     35    class << self 
     36      def test_condition(expression, condition) 
     37        case condition 
     38          when String then "(#{expression} == #{condition.inspect})" 
     39          when Regexp then 
     40            condition = Regexp.new("^#{condition.source}$") unless /^\^.*\$$/ =~ condition.source  
     41            "(#{condition.inspect} =~ #{expression})" 
     42          when true then expression 
     43          when nil then "! #{expression}" 
    1744          else 
    18             (@items.include?(k) ? @defaults : @requirements)[k] = (v.nil? ? nil : v.to_s) 
    19           end 
    20         end 
    21          
    22         @defaults.each do |k, v| 
    23           raise ArgumentError, "A default has been specified for #{k}, but #{k} is not in the path!" unless @items.include? k 
    24           @defaults[k] = v.to_s unless v.kind_of?(String) || v.nil? 
    25         end 
    26         @requirements.each {|k, v| raise ArgumentError, "A Regexp requirement has been specified for #{k}, but #{k} is not in the path!" if v.kind_of?(Regexp) && ! @items.include?(k)} 
    27          
    28         # Add in defaults for :action and :id. 
    29         [[:action, 'index'], [:id, nil]].each do |name, default| 
    30           @defaults[name] = default if @items.include?(name) && ! (@requirements.key?(name) || @defaults.key?(name)) 
    31         end 
    32       end 
    33        
    34       # Generate a URL given the provided options. 
    35       # All values in options should be symbols. 
    36       # Returns the path and the unused names in a 2 element array. 
    37       # If generation fails, [nil, nil] is returned 
    38       # Generation can fail because of a missing value, or because an equality check fails. 
    39       # 
    40       # Generate urls will be as short as possible. If the last component of a url is equal to the default value, 
    41       # then that component is removed. This is applied as many times as possible. So, your index controller's 
    42       # index action will generate [] 
    43       def generate(options, defaults={}) 
    44         non_matching = @requirements.keys.select {|name| ! passes_requirements?(name, options[name] || defaults[name])} 
    45         non_matching.collect! {|name| requirements_for(name)} 
    46         return nil, "Mismatching option#{'s' if non_matching.length > 1}:\n   #{non_matching.join '\n   '}" unless non_matching.empty? 
    47          
    48         used_names = @requirements.inject({}) {|hash, (k, v)| hash[k] = true; hash} # Mark requirements as used so they don't get put in the query params 
    49         components = @items.collect do |item| 
    50  
    51           if item.kind_of? Symbol 
    52             collection = false 
    53  
    54             if /^\*/ =~ item.to_s 
    55               collection = true 
    56               item = item.to_s.sub(/^\*/,"").intern 
     45            raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil" 
     46        end 
     47      end 
     48    end 
     49 
     50    class Component #:nodoc 
     51      def dynamic?()  false end 
     52      def optional?() false end 
     53 
     54      def key() nil end 
     55   
     56      def self.new(string, *args) 
     57        return super(string, *args) unless self == Component 
     58        case string 
     59          when ':controller' then ControllerComponent.new(:controller, *args) 
     60          when /^:(\w+)$/    then DynamicComponent.new($1, *args) 
     61          when /^\*(\w+)$/   then PathComponent.new($1, *args) 
     62          else StaticComponent.new(string, *args) 
     63        end 
     64      end  
     65    end 
     66 
     67    class StaticComponent < Component #:nodoc 
     68      attr_reader :value 
     69   
     70      def initialize(value) 
     71        @value = value 
     72      end 
     73 
     74      def write_recognition(g) 
     75        g.if_next_matches(value) do |gp| 
     76          gp.move_forward {|gpp| gpp.continue} 
     77        end 
     78      end 
     79 
     80      def write_generation(g) 
     81        g.add_segment(value) {|gp| gp.continue } 
     82      end 
     83    end 
     84 
     85    class DynamicComponent < Component #:nodoc 
     86      attr_reader :key, :default 
     87      attr_accessor :condition 
     88   
     89      def dynamic?()  true      end 
     90      def optional?() @optional end 
     91 
     92      def default=(default) 
     93        @optional = true 
     94        @default = default 
     95      end 
     96     
     97      def initialize(key, options = {}) 
     98        @key = key.to_sym 
     99        @default, @condition = options[:default], options[:condition] 
     100        @optional = options.key?(:default) 
     101      end 
     102 
     103      def default_check(g) 
     104        presence = "#{g.hash_value(key, !! default)}" 
     105        if default 
     106           "!(#{presence} && #{g.hash_value(key, false)} != #{default.inspect})" 
     107        else 
     108          "! #{presence}" 
     109        end 
     110      end 
     111   
     112      def write_generation(g) 
     113        wrote_dropout = write_dropout_generation(g) 
     114        write_continue_generation(g, wrote_dropout) 
     115      end 
     116 
     117      def write_dropout_generation(g) 
     118        return false unless optional? && g.after.all? {|c| c.optional?} 
     119     
     120        check = [default_check(g)] 
     121        gp = g.dup # Use another generator to write the conditions after the first && 
     122        # We do this to ensure that the generator will not assume x_value is set. It will 
     123        # not be set if it follows a false condition -- for example, false && (x = 2) 
     124     
     125        gp.after.map {|c| c.default_check gp} 
     126        gp.if(check.join(' && ')) { gp.finish } # If this condition is met, we stop here 
     127        true  
     128      end 
     129 
     130      def write_continue_generation(g, use_else) 
     131        test  = Routing.test_condition(g.hash_value(key, true, default), condition || true) 
     132        check = (use_else && condition.nil? && default) ? [:else] : [use_else ? :elsif : :if, test] 
     133     
     134        g.send(*check) do |gp| 
     135          gp.expire_for_keys(key) unless gp.after.empty? 
     136          add_segments_to(gp) {|gpp| gpp.continue} 
     137        end 
     138      end 
     139 
     140      def add_segments_to(g) 
     141        g.add_segment(%(\#{CGI.escape(#{g.hash_value(key, true, default)})})) {|gp| yield gp} 
     142      end 
     143   
     144      def recognition_check(g) 
     145        test_type = [true, nil].include?(condition) ? :presence : :constraint 
     146     
     147        prefix = condition.is_a?(Regexp) ? "#{g.next_segment(true)} && " : '' 
     148        check = prefix + Routing.test_condition(g.next_segment(true), condition || true) 
     149     
     150        g.if(check) {|gp| yield gp, test_type} 
     151      end 
     152   
     153      def write_recognition(g) 
     154        test_type = nil 
     155        recognition_check(g) do |gp, test_type| 
     156          assign_result(gp) {|gpp| gpp.continue} 
     157        end 
     158     
     159        if optional? && g.after.all? {|c| c.optional?} 
     160          call = (test_type == :presence) ? [:else] : [:elsif, "! #{g.next_segment(true)}"] 
     161        
     162          g.send(*call) do |gp| 
     163            assign_default(gp) 
     164            gp.after.each {|c| c.assign_default(gp)} 
     165            gp.finish(false) 
     166          end 
     167        end 
     168      end 
     169 
     170      def assign_result(g, with_default = false) 
     171        g.result key, "CGI.unescape(#{g.next_segment(true, with_default ? default : nil)})" 
     172        g.move_forward {|gp| yield gp} 
     173      end 
     174 
     175      def assign_default(g) 
     176        g.constant_result key, default unless default.nil? 
     177      end 
     178    end 
     179 
     180    class ControllerComponent < DynamicComponent #:nodoc 
     181      def key() :controller end 
     182 
     183      def add_segments_to(g) 
     184        g.add_segment(%(\#{#{g.hash_value(key, true, default)}})) {|gp| yield gp} 
     185      end 
     186     
     187      def recognition_check(g) 
     188        g << "controller_result = ::ActionController::Routing::ControllerComponent.traverse_to_controller(#{g.path_name}, #{g.index_name})"  
     189        g.if('controller_result') do |gp| 
     190          gp << 'controller_value, segments_to_controller = controller_result' 
     191          gp.move_forward('segments_to_controller') {|gpp| yield gpp, :constraint} 
     192        end 
     193      end 
     194 
     195      def assign_result(g) 
     196        g.result key, 'controller_value' 
     197        yield g 
     198      end 
     199 
     200      def assign_default(g) 
     201        ControllerComponent.assign_controller(g, default) 
     202      end 
     203   
     204      class << self 
     205        def assign_controller(g, controller) 
     206          expr = "::Controllers::#{controller.split('/').collect {|c| c.camelize}.join('::')}Controller" 
     207          g.result :controller, expr, true 
     208        end 
     209 
     210        def traverse_to_controller(segments, start_at = 0) 
     211          mod = ::Controllers 
     212          length = segments.length 
     213          index = start_at 
     214          mod_name = controller_name = segment = nil 
     215       
     216          while index < length 
     217            return nil unless /^[a-z][a-z\d_]*$/ =~ (segment = segments[index]) 
     218            index += 1 
     219         
     220            mod_name = segment.camelize 
     221            controller_name = "#{mod_name}Controller" 
     222         
     223            return eval("mod::#{controller_name}"), (index - start_at) if mod.const_available?(controller_name) 
     224            return nil unless mod.const_available?(mod_name) 
     225            mod = eval("mod::#{mod_name}") 
     226          end 
     227        end 
     228      end 
     229    end 
     230 
     231    class PathComponent < DynamicComponent #:nodoc  
     232      def optional?() true end 
     233      def default()   ''   end 
     234      def condition() nil  end 
     235   
     236      def write_generation(g) 
     237        raise RoutingError, 'Path components must occur last' unless g.after.empty? 
     238        g.if("#{g.hash_value(key, true)} && ! #{g.hash_value(key, true)}.empty?") do 
     239          g << "#{g.hash_value(key, true)} = #{g.hash_value(key, true)}.join('/') unless #{g.hash_value(key, true)}.is_a?(String)" 
     240          g.add_segment("\#{CGI.escape_skipping_slashes(#{g.hash_value(key, true)})}") {|gp| gp.finish } 
     241        end 
     242        g.else { g.finish } 
     243      end 
     244   
     245      def write_recognition(g) 
     246        raise RoutingError, "Path components must occur last" unless g.after.empty? 
     247     
     248        start = g.index_name 
     249        start = "(#{start})" unless /^\w+$/ =~ start 
     250     
     251        value_expr = "#{g.path_name}[#{start}..-1] || []" 
     252        g.result key, "ActionController::Routing::PathComponent::Result.new(#{value_expr})" 
     253        g.finish(false) 
     254      end 
     255   
     256      class Result < ::Array 
     257        def to_s() join '/' end 
     258      end 
     259    end 
     260 
     261    class Route 
     262      attr_accessor :components, :known 
     263      attr_reader :path, :options, :keys 
     264   
     265      def initialize(path, options = {}) 
     266        @path, @options = path, options 
     267     
     268        initialize_components path 
     269        defaults, conditions = initialize_hashes options.dup 
     270        configure_components(defaults, conditions) 
     271        initialize_keys 
     272      end 
     273   
     274      def inspect 
     275        "<#{self.class} #{path.inspect}, #{options.inspect[1..-1]}>" 
     276      end 
     277   
     278      def write_generation(generator = CodeGeneration::GenerationGenerator.new) 
     279        generator.before, generator.current, generator.after = [], components.first, (components[1..-1] || []) 
     280 
     281        if known.empty? then generator.go 
     282        else generator.if(generator.check_conditions(known)) {|gp| gp.go } 
     283        end 
     284     
     285        generator 
     286      end 
     287   
     288      def write_recognition(generator = CodeGeneration::RecognitionGenerator.new) 
     289        g = generator.dup 
     290        g.share_locals_with generator 
     291        g.before, g.current, g.after = [], components.first, (components[1..-1] || []) 
     292     
     293        known.each do |key, value| 
     294          if key == :controller then ControllerComponent.assign_controller(g, value) 
     295          else g.constant_result(key, value) 
     296          end 
     297        end 
     298     
     299        g.go 
     300     
     301        generator 
     302      end 
     303 
     304      def initialize_keys 
     305        @keys = (components.collect {|c| c.key} + known.keys).compact 
     306        @keys.freeze 
     307      end 
     308   
     309      def extra_keys(options) 
     310        options.keys - @keys 
     311      end 
     312     
     313      def matches_controller?(controller) 
     314        if known[:controller] then known[:controller] == controller 
     315        else 
     316          c = components.find {|c| c.key == :controller} 
     317          return false unless c 
     318          return c.condition.nil? || eval(Routing.test_condition('controller', c.condition)) 
     319        end 
     320      end 
     321   
     322      protected 
     323   
     324        def initialize_components(path) 
     325          path = path.split('/') if path.is_a? String 
     326          self.components = path.collect {|str| Component.new str} 
     327        end 
     328     
     329        def initialize_hashes(options) 
     330          path_keys = components.collect {|c| c.key }.compact  
     331          self.known = {} 
     332          defaults = options.delete(:defaults) || {} 
     333          conditions = options.delete(:require) || {} 
     334          conditions.update(options.delete(:requirements) || {}) 
     335       
     336          options.each do |k, v| 
     337            if path_keys.include?(k) then (v.is_a?(Regexp) ? conditions : defaults)[k] = v 
     338            else known[k] = v 
    57339            end 
    58  
    59             used_names[item] = true 
    60             value = options[item] || defaults[item] || @defaults[item] 
    61             return nil, requirements_for(item) unless passes_requirements?(item, value) 
    62  
    63             defaults = {} unless defaults == {} || value == defaults[item] # Stop using defaults if this component isn't the same as the default. 
    64  
    65             if value.nil? || item == :controller 
    66               value 
    67             elsif collection 
    68               if value.kind_of?(Array) 
    69                 value = value.collect {|v| Routing.extract_parameter_value(v)}.join('/') 
    70               else 
    71                 value = Routing.extract_parameter_value(value).gsub(/%2F/, "/") 
    72               end 
    73               value 
    74             else 
    75               Routing.extract_parameter_value(value) 
     340          end 
     341          [defaults, conditions] 
     342        end 
     343     
     344        def configure_components(defaults, conditions) 
     345          components.each do |component| 
     346            if defaults.key?(component.key) then component.default = defaults[component.key] 
     347            elsif component.key == :action  then component.default = 'index' 
     348            elsif component.key == :id      then component.default = nil 
    76349            end 
    77           else 
    78             item 
    79           end 
    80         end 
    81          
    82         @items.reverse_each do |item| # Remove default components from the end of the generated url. 
    83           break unless item.kind_of?(Symbol) && @defaults[item] == components.last 
    84           components.pop 
    85         end 
    86          
    87         # If we have any nil components then we can't proceed. 
    88         # This might need to be changed. In some cases we may be able to return all componets after nil as extras. 
    89         missing = []; components.each_with_index {|c, i| missing << @items[i] if c.nil?} 
    90         return nil, "No values provided for component#{'s' if missing.length > 1} #{missing.join ', '} but values are required due to use of later components" unless missing.empty? # how wide is your screen? 
    91          
    92         unused = (options.keys - used_names.keys).inject({}) do |unused, key| 
    93           unused[key] = options[key] if options[key] != @defaults[key] 
    94           unused 
    95         end 
    96          
    97         components.collect! {|c| c.to_s} 
    98         return components, unused 
    99       end 
    100        
    101       # Recognize the provided path, returning a hash of recognized values, or [nil, reason] if the path isn't recognized. 
    102       # The path should be a list of component strings. 
    103       # Options is a hash of the ?k=v pairs 
    104       def recognize(components, options={}) 
    105         options = options.clone 
    106         components = components.clone 
    107         controller_class = nil 
    108          
    109         @items.each do |item| 
    110           if item == :controller # Special case for controller 
    111             if components.empty? && @defaults[:controller] 
    112               controller_class, leftover = eat_path_to_controller(@defaults[:controller].split('/')) 
    113               raise RoutingError, "Default controller does not exist: #{@defaults[:controller]}" if controller_class.nil? || leftover.empty? == false 
    114             else 
    115               controller_class, remaining_components = eat_path_to_controller(components) 
    116               return nil, "No controller found at subpath #{components.join('/')}" if controller_class.nil? 
    117               components = remaining_components 
    118             end 
    119             options[:controller] = controller_class.controller_path 
    120             return nil, requirements_for(:controller) unless passes_requirements?(:controller, options[:controller]) 
    121           elsif /^\*/ =~ item.to_s 
    122             if components.empty? 
    123               value = @defaults.has_key?(item) ? @defaults[item].clone : [] 
    124             else 
    125               value = components.clone 
    126             end 
    127             value.collect! {|c| CGI.unescape c} 
    128             components = [] 
    129             def value.to_s() self.join('/') end 
    130             options[item.to_s.sub(/^\*/,"").intern] = value 
    131           elsif item.kind_of? Symbol 
    132             value = components.shift || @defaults[item] 
    133             return nil, requirements_for(item) unless passes_requirements?(item, value) 
    134             options[item] = value.nil? ? value : CGI.unescape(value) 
    135           else 
    136             return nil, "No value available for component #{item.inspect}" if components.empty? 
    137             component = components.shift 
    138             return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item 
    139           end 
    140         end 
    141          
    142         if controller_class.nil? && @requirements[:controller] # Load a default controller 
    143           controller_class, extras = eat_path_to_controller(@requirements[:controller].split('/')) 
    144           raise RoutingError, "Illegal controller path for route default: #{@requirements[:controller]}" unless controller_class && extras.empty? 
    145           options[:controller] = controller_class.controller_path 
    146         end 
    147         @requirements.each {|k,v| options[k] ||= v unless v.kind_of?(Regexp)} 
    148  
    149         return nil, "Route recognition didn't find a controller class!" unless controller_class 
    150         return nil, "Unused components were left: #{components.join '/'}" unless components.empty? 
    151         options.delete_if {|k, v| v.nil?} # Remove nil values. 
    152         return controller_class, options 
    153       end 
    154        
    155       def inspect 
    156         when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}" 
    157         default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}" 
    158         "<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join('/').inspect}#{default_str}#{when_str}>" 
    159       end 
    160        
    161       protected 
    162         # Find the controller given a list of path components. 
    163         # Return the controller class and the unused path components. 
    164         def eat_path_to_controller(path) 
    165           path.inject([Controllers, 1]) do |(mod, length), name| 
    166             name = name.camelize 
    167             return nil, nil unless /^[A-Z][_a-zA-Z\d]*$/ =~ name 
    168             controller_name = name + "Controller" 
    169             return eval("mod::#{controller_name}"), path[length..-1] if mod.const_available? controller_name 
    170             return nil, nil unless mod.const_available? name 
    171             [mod.const_get(name), length + 1] 
    172           end 
    173           return nil, nil # Path ended, but no controller found. 
    174         end 
    175        
    176         def items=(path) 
    177           items = path.split('/').collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".intern) : c} if path.kind_of?(String) # split and convert ':xyz' to symbols 
    178           items.shift if items.first == "" 
    179           items.pop if items.last == "" 
    180           @items = items 
    181            
    182           # Verify uniqueness of each component. 
    183           @items.inject({}) do |seen, item| 
    184             if item.kind_of? Symbol 
    185               raise ArgumentError, "Illegal route path -- duplicate item #{item}\n   #{path.inspect}" if seen.key? item 
    186               seen[item] = true 
    187             end 
    188             seen 
    189           end 
    190         end 
    191          
    192         # Verify that the given value passes this route's requirements 
    193         def passes_requirements?(name, value) 
    194           return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be 
    195            
    196           case @requirements[name] 
    197             when nil then true 
    198             when Regexp then 
    199               value = value.to_s 
    200               match = @requirements[name].match(value) 
    201               match && match[0].length == value.length 
    202             else 
    203               @requirements[name] == value.to_s 
    204           end 
    205         end 
    206         def requirements_for(name) 
    207           name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect) 
    208           presence = (@defaults.key?(name) && @defaults[name].nil?) 
    209           requirement = case @requirements[name] 
    210             when nil then nil 
    211             when Regexp then "match #{@requirements[name].inspect}" 
    212             else "be equal to #{@requirements[name].inspect}" 
    213           end 
    214           if presence && requirement then "#{name} must be present and #{requirement}" 
    215           elsif presence || requirement then "#{name} must #{requirement || 'be present'}" 
    216           else "#{name} has no requirements" 
    217           end 
    218         end 
    219     end 
    220      
    221     class RouteSet#:nodoc: 
     350         
     351            component.condition = conditions[component.key] if conditions.key?(component.key) 
     352          end 
     353        end 
     354    end 
     355 
     356    class RouteSet 
     357      attr_reader :routes, :categories, :controller_to_selector 
    222358      def initialize 
    223359        @routes = [] 
    224       end 
    225        
    226       def add_route(route) 
    227         raise TypeError, "#{route.inspect} is not a Route instance!" unless route.kind_of?(Route) 
    228         @routes << route 
    229       end 
    230       def empty? 
    231         @routes.empty? 
    232       end 
    233       def each 
    234         @routes.each {|route| yield route} 
    235       end 
    236        
    237       # Generate a path for the provided options 
    238       # Returns the path as an array of components and a hash of unused names 
    239       # Raises RoutingError if not route can handle the provided components. 
    240       # 
    241       # Note that we don't return the first generated path. We do this so that when a route 
    242       # generates a path from a subset of the available options we can keep looking for a  
    243       # route which can generate a path that uses more options. 
    244       # Note that we *do* return immediately if  
    245       def generate(options, request) 
    246         raise RoutingError, "There are no routes defined!" if @routes.empty? 
    247  
    248         options = options.symbolize_keys 
    249         defaults = request.path_parameters.symbolize_keys 
    250         if options.empty? then options = defaults.clone # Get back the current url if no options was passed 
    251         else expand_controller_path!(options, defaults) # Expand the supplied controller path. 
    252         end 
    253         defaults.delete_if {|k, v| options.key?(k) && options[k].nil?} # Remove defaults that have been manually cleared using :name => nil 
    254  
    255         failures = [] 
    256         selected = nil 
    257         self.each do |route| 
    258           path, unused = route.generate(options, defaults) 
    259           if path.nil? 
    260             failures << [route, unused] if ActionController::Base.debug_routes 
    261           else  
    262             return path, unused if unused.empty? # Found a perfect route -- we're finished. 
    263             if selected.nil? || unused.length < selected.last.length 
    264               failures << [selected.first, "A better url than #{selected[1]} was found."] if selected 
    265               selected = [route, path, unused] 
     360        @generation_methods = Hash.new(:generate_default_path) 
     361      end 
     362       
     363      def generate(options, request_or_recall_hash = {}) 
     364        recall = request_or_recall_hash.is_a?(Hash) ? request_or_recall_hash : request_or_recall_hash.symbolized_path_parameters 
     365         
     366        if ((rc_c = recall[:controller]) && rc_c.include?(?/)) || ((c = options[:controller]) && c.include?(?/))   
     367          options[:controller] = Routing.controller_relative_to(c, rc_c) 
     368        end 
     369        options = recall.dup if options.empty? # XXX move to url_rewriter? 
     370        Routing.treat_hash(options) # XXX Move inwards (to generated code) or inline? 
     371        merged = recall.merge(options) 
     372        expire_on = Routing.expiry_hash(options, recall) 
     373     
     374        path, keys = generate_path(merged, options, expire_on) 
     375     
     376        # Factor out? 
     377        extras = {} 
     378        k = nil 
     379        keys.each {|k| extras[k] = options[k]}  
     380        [path, extras] 
     381      end 
     382       
     383      def generate_path(merged, options, expire_on) 
     384        send @generation_methods[merged[:controller]], merged, options, expire_on 
     385      end 
     386   
     387      def write_generation 
     388        @generation_methods = Hash.new(:generate_default_path) 
     389        categorize_routes.each do |controller, routes| 
     390          next unless routes.length < @routes.length 
     391       
     392          ivar = controller.gsub('/', '__') 
     393          method_name = "generate_path_for_#{ivar}".to_sym 
     394          instance_variable_set "@#{ivar}", routes 
     395          code = generation_code_for(ivar, method_name).to_s 
     396          eval(code) 
     397       
     398          @generation_methods[controller.to_s]   = method_name 
     399          @generation_methods[controller.to_sym] = method_name 
     400        end 
     401         
     402        eval(generation_code_for('routes', 'generate_default_path').to_s) 
     403      end 
     404 
     405      def recognize(request) 
     406        string_path = request.path 
     407        string_path.chomp! if string_path[0] == ?/ 
     408        path = string_path.split '/' 
     409        path.shift 
     410     
     411        hash = recognize_path(path) 
     412        raise RoutingError, "No route matches path #{path.inspect}" unless hash 
     413     
     414        controller = hash['controller'] 
     415        hash['controller'] = controller.controller_path 
     416        request.path_parameters = hash 
     417        controller.new 
     418      end 
     419      alias :recognize! :recognize 
     420   
     421      def write_recognition 
     422        g = generator = CodeGeneration::RecognitionGenerator.new 
     423        g.finish_statement = Proc.new {|hash_expr| "return #{hash_expr}"} 
     424     
     425        g.def "self.recognize_path(path)" do 
     426          each do |route| 
     427            g << 'index = 0' 
     428            route.write_recognition(g) 
     429          end 
     430        end 
     431     
     432        eval g.to_s  
     433      end 
     434         
     435      def generation_code_for(ivar = 'routes', method_name = nil) 
     436        routes = instance_variable_get('@' + ivar) 
     437        key_ivar = "@keys_for_#{ivar}" 
     438        instance_variable_set(key_ivar, routes.collect {|route| route.keys}) 
     439     
     440        g = generator = CodeGeneration::GenerationGenerator.new 
     441        g.def "self.#{method_name}(merged, options, expire_on)" do 
     442          g << 'unused_count = options.length + 1' 
     443          g << "unused_keys = keys = options.keys" 
     444          g << 'path = nil' 
     445       
     446          routes.each_with_index do |route, index| 
     447            g << "new_unused_keys = keys - #{key_ivar}[#{index}]" 
     448            g << 'new_path = (' 
     449            g.source.indent do 
     450              if index.zero? 
     451                g << "new_unused_count = new_unused_keys.length" 
     452                g << "hash = merged; not_expired = true" 
     453                route.write_generation(g.dup) 
     454              else 
     455                g.if "(new_unused_count = new_unused_keys.length) < unused_count" do |gp| 
     456                  gp << "hash = merged; not_expired = true" 
     457                  route.write_generation(gp) 
     458                end 
     459              end 
    266460            end 
    267           end 
    268         end 
    269          
    270         return selected[1..-1] unless selected.nil? 
    271         raise RoutingError.new("Generation failure: No route for url_options #{options.inspect}, defaults: #{defaults.inspect}", failures) 
    272       end 
    273        
    274       # Recognize the provided path. 
    275       # Raise RoutingError if the path can't be recognized. 
    276       def recognize!(request) 
    277         path = ((%r{^/?(.*)/?$} =~ request.path) ? $1 : request.path).split('/') 
    278         raise RoutingError, "There are no routes defined!" if @routes.empty? 
    279          
    280         failures = [] 
    281         self.each do |route| 
    282           controller, options = route.recognize(path) 
    283           if controller.nil? 
    284             failures << [route, options] if ActionController::Base.debug_routes 
    285           else 
    286             request.path_parameters = options 
    287             return controller 
    288           end 
    289         end 
    290          
    291         raise RoutingError.new("No route for path: #{path.join('/').inspect}", failures) 
    292       end 
    293        
    294       def expand_controller_path!(options, defaults) 
    295         if options[:controller] 
    296           if /^\// =~ options[:controller] 
    297             options[:controller] = options[:controller][1..-1] 
    298             defaults.clear # Sending to absolute controller implies fresh defaults 
    299           else 
    300             relative_to = defaults[:controller] ? defaults[:controller].split('/')[0..-2].join('/') : '' 
    301             options[:controller] = relative_to.empty? ? options[:controller] : "#{relative_to}/#{options[:controller]}" 
    302             defaults.delete(:action) if options.key?(:controller) 
    303           end 
    304         else 
    305           options[:controller] = defaults[:controller] 
    306         end 
    307       end 
    308        
    309       def route(*args) 
    310         add_route(Route.new(*args)) 
    311       end 
    312       alias :connect :route 
    313        
     461            g.source.lines.last << ' )' # Add the closing brace to the end line 
     462            g.if 'new_path' do 
     463              g << 'return new_path, [] if new_unused_count.zero?' 
     464              g << 'path = new_path; unused_keys = new_unused_keys; unused_count = new_unused_count' 
     465            end 
     466          end 
     467         
     468          g << "raise RoutingError, \"No url can be generated for the hash \#{options.inspect}\" unless path" 
     469          g << "return path, unused_keys" 
     470        end 
     471         
     472        return g 
     473      end 
     474       
     475      def categorize_routes 
     476        @categorized_routes = by_controller = Hash.new(self) 
     477       
     478        known_controllers.each do |name| 
     479          set = by_controller[name] = [] 
     480          each do |route| 
     481            set << route if route.matches_controller? name 
     482          end 
     483        end 
     484     
     485        @categorized_routes 
     486      end 
     487       
     488      def known_controllers 
     489        @routes.inject([]) do |known, route| 
     490          if (controller = route.known[:controller]) 
     491            if controller.is_a?(Regexp) 
     492              known << controller.source.scan(%r{[\w\d/]+}).select {|word| controller =~ word}  
     493            else known << controller 
     494            end 
     495          end 
     496          known 
     497        end.uniq 
     498      end 
     499 
    314500      def reload 
    315         begin 
    316           route_file = defined?(RAILS_ROOT) ? File.join(RAILS_ROOT, 'config', 'routes') : nil 
    317           require_dependency(route_file) if route_file 
    318         rescue LoadError, ScriptError => e 
    319           raise RoutingError.new("Cannot load config/routes.rb:\n    #{e.message}").copy_blame!(e) 
    320         ensure # Ensure that there is at least one route: 
    321           connect(':controller/:action/:id', :action => 'index', :id => nil) if @routes.empty? 
    322         end 
    323       end 
    324        
     501        NamedRoutes.clear 
     502        load(File.join(RAILS_ROOT, 'config', 'routes.rb')) 
     503        NamedRoutes.install 
     504      end 
     505 
     506      def connect(*args) 
     507        new_route = Route.new(*args) 
     508        @routes << new_route 
     509        return new_route 
     510      end 
     511 
    325512      def draw 
    326         @routes.clear 
    327         yield self 
    328       end 
    329     end 
    330      
    331     def self.extract_parameter_value(parameter) #:nodoc: 
    332       value = (parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s 
    333       CGI.escape(value) 
    334     end 
    335  
    336     def self.draw(*args, &block) #:nodoc: 
    337       Routes.draw(*args) {|*args| block.call(*args)} 
    338     end 
    339      
     513        old_routes = @routes 
     514        @routes = [] 
     515         
     516        begin yield self 
     517        rescue 
     518          @routes = old_routes 
     519          raise 
     520        end 
     521        write_generation 
     522        write_recognition 
     523      end 
     524       
     525      def empty?() @routes.empty? end 
     526   
     527      def each(&block) @routes.each(&block) end 
     528       
     529      def method_missing(name, *args) 
     530        return super(name, *args) unless args.length == 2 
     531       
     532        route = connect(*args) 
     533        NamedRoutes.name_route(route, name) 
     534        route 
     535      end 
     536    end 
     537 
     538    module NamedRoutes 
     539      Helpers = [] 
     540      class << self 
     541        def clear() Helpers.clear end 
     542   
     543        def hash_access_name(name) 
     544          "hash_for_#{name}_url" 
     545        end 
     546 
     547        def url_helper_name(name) 
     548          "#{name}_url" 
     549        end 
     550 
     551        def name_route(route, name) 
     552          hash = route.known.symbolize_keys 
     553       
     554          define_method(hash_access_name(name)) { hash } 
     555          module_eval(%{def #{url_helper_name name}(options = {}) 
     556            url_for(#{hash_access_name(name)}.merge(options)) 
     557          end}) 
     558       
     559          protected url_helper_name(name), hash_access_name(name) 
     560       
     561          Helpers << url_helper_name(name) 
     562          Helpers.uniq! 
     563        end 
     564     
     565        def install(cls = ActionController::Base) 
     566          cls.send :include, self 
     567          if cls.respond_to? :helper_method 
     568            Helpers.each do |helper_name| 
     569              cls.send :helper_method, helper_name 
     570            end 
     571          end 
     572        end 
     573      end 
     574    end 
     575 
    340576    Routes = RouteSet.new 
    341577  end 
  • trunk/actionpack/lib/action_controller/url_rewriter.rb

    r1193 r1496  
    1313 
    1414    def to_str 
    15                "#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}" 
     15      "#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}" 
    1616    end 
    1717 
     
    2929        rewritten_url << "##{options[:anchor]}" if options[:anchor] 
    3030 
    31         return rewritten_url 
     31        rewritten_url 
    3232      end 
    3333 
     
    3939 
    4040        if extras[:overwrite_params] 
    41           params_copy = @request.parameters.reject { |k,v| ["controller","action"].include? k } 
     41          params_copy = @request.parameters.reject { |k,v| %w(controller action).include? k } 
    4242          params_copy.update extras[:overwrite_params] 
    4343          extras.delete(:overwrite_params)