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

Changeset 4697

Show
Ignore:
Timestamp:
08/07/06 06:09:18 (2 years ago)
Author:
ulysses
Message:

that wasnt me

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • branches/stable/actionpack/lib/action_controller/routing.rb

    r4696 r4697  
    1 require 'cgi' 
    2  
    3 class Object 
    4   def to_param 
    5     to_s 
    6   end 
    7 end 
    8  
    9 class TrueClass 
    10   def to_param 
    11     self 
    12   end 
    13 end 
    14  
    15 class FalseClass 
    16   def to_param 
    17     self 
    18   end 
    19 end 
    20  
    21 class NilClass 
    22   def to_param 
    23     self 
    24   end 
    25 end 
    26  
    27 class Regexp 
    28   def number_of_captures 
    29     Regexp.new("|#{source}").match('').captures.length 
    30   end 
    31    
    32   class << self 
    33     def optionalize(pattern) 
    34       case unoptionalize(pattern) 
    35         when /\A(.|\(.*\))\Z/ then "#{pattern}?" 
    36         else "(?:#{pattern})?" 
    37       end 
    38     end 
    39      
    40     def unoptionalize(pattern) 
    41       [/\A\(\?:(.*)\)\?\Z/, /\A(.|\(.*\))\?\Z/].each do |regexp| 
    42         return $1 if regexp =~ pattern 
    43       end 
    44       return pattern 
    45     end 
    46   end 
    47 end 
    48  
    491module ActionController 
    50   module Routing 
    51     SEPARATORS = %w( / ; . , ? ) 
    52  
     2  module Routing #:nodoc: 
    533    class << self 
    54       def with_controllers(names) 
    55         use_controllers! names 
    56         yield 
    57       ensure 
    58         use_controllers! nil 
    59       end 
    60  
    61       def normalize_paths(paths = $LOAD_PATH) 
    62         # do the hokey-pokey of path normalization... 
    63         paths = paths.collect do |path| 
    64           path = path. 
    65             gsub("//", "/").           # replace double / chars with a single 
    66             gsub("\\\\", "\\").        # replace double \ chars with a single 
    67             gsub(%r{(.)[\\/]$}, '\1')  # drop final / or \ if path ends with it 
    68  
    69           # eliminate .. paths where possible 
    70           re = %r{\w+[/\\]\.\.[/\\]} 
    71           path.gsub!(%r{\w+[/\\]\.\.[/\\]}, "") while path.match(re) 
    72           path 
    73         end 
    74  
    75         # start with longest path, first 
    76         paths = paths.uniq.sort_by { |path| - path.length } 
    77       end 
    78  
    79       def possible_controllers 
    80         unless @possible_controllers 
    81           @possible_controllers = [] 
    82          
    83           paths = $LOAD_PATH.select { |path| File.directory?(path) && path != "." } 
    84  
    85           seen_paths = Hash.new {|h, k| h[k] = true; false} 
    86           normalize_paths(paths).each do |load_path| 
    87             Dir["#{load_path}/**/*_controller.rb"].collect do |path| 
    88               next if seen_paths[path.gsub(%r{^\.[/\\]}, "")] 
    89              
    90               controller_name = path[(load_path.length + 1)..-1] 
    91               next unless path_may_be_controller?(controller_name) 
    92  
    93               controller_name.gsub!(/_controller\.rb\Z/, '') 
    94               @possible_controllers << controller_name 
    95             end 
    96           end 
    97  
    98           # remove duplicates 
    99           @possible_controllers.uniq! 
    100         end 
    101         @possible_controllers 
    102       end 
    103  
    104       def path_may_be_controller?(path) 
    105         path !~ /(?:rails\/.*\/(?:examples|test))|(?:actionpack\/lib\/action_controller.rb$)|(?:app\/controllers)/o 
    106       end 
    107  
    108       def use_controllers!(controller_names) 
    109         @possible_controllers = controller_names 
    110       end 
    111  
     4      def expiry_hash(options, recall) 
     5        k = v = nil 
     6        expire_on = {} 
     7        options.each {|k, v| expire_on[k] = ((rcv = recall[k]) && (rcv != v))} 
     8        expire_on 
     9      end 
     10 
     11      def extract_parameter_value(parameter) #:nodoc: 
     12        CGI.escape((parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s)  
     13      end 
    11214      def controller_relative_to(controller, previous) 
    11315        if controller.nil?           then previous 
     
    11517        elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}" 
    11618        else controller 
    117         end      
    118       end      
    119     end 
    120    
    121     class Route 
    122       attr_accessor :segments, :requirements, :conditions 
    123        
    124       def initialize 
    125         @segments = [] 
    126         @requirements = {} 
    127         @conditions = {} 
    128       end 
    129    
    130       # Write and compile a +generate+ method for this Route. 
    131       def write_generation 
    132         # Build the main body of the generation 
    133         body = "not_expired = true\n#{generation_extraction}\n#{generation_structure}" 
    134      
    135         # If we have conditions that must be tested first, nest the body inside an if 
    136         body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements 
    137         args = "options, hash, expire_on = {}" 
    138  
    139         # Nest the body inside of a def block, and then compile it. 
    140         raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend" 
    141         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" 
    142  
    143         # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash 
    144         # are the same as the keys that were recalled from the previous request. Thus, 
    145         # we can use the expire_on.keys to determine which keys ought to be used to build 
    146         # the query string. (Never use keys from the recalled request when building the 
    147         # query string.) 
    148  
    149         method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(hash, expire_on))\nend" 
    150         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" 
    151  
    152         method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(hash, expire_on)]\nend" 
    153         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" 
    154         raw_method 
    155       end 
    156    
    157       # Build several lines of code that extract values from the options hash. If any 
    158       # of the values are missing or rejected then a return will be executed. 
    159       def generation_extraction 
    160         segments.collect do |segment| 
    161           segment.extraction_code 
    162         end.compact * "\n" 
    163       end 
    164    
    165       # Produce a condition expression that will check the requirements of this route 
    166       # upon generation. 
    167       def generation_requirements 
    168         requirement_conditions = requirements.collect do |key, req| 
    169           if req.is_a? Regexp 
    170             value_regexp = Regexp.new "\\A#{req.source}\\Z" 
    171             "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]" 
     19        end 
     20      end 
     21 
     22      def treat_hash(hash, keys_to_delete = []) 
     23        k = v = nil 
     24        hash.each do |k, v| 
     25          if v then hash[k] = (v.respond_to? :to_param) ? v.to_param.to_s : v.to_s 
    17226          else 
    173             "hash[:#{key}] == #{req.inspect}" 
    174           end 
    175         end 
    176         requirement_conditions * ' && ' unless requirement_conditions.empty? 
    177       end 
    178       def generation_structure 
    179         segments.last.string_structure segments[0..-2] 
    180       end 
    181    
    182       # Write and compile a +recognize+ method for this Route. 
    183       def write_recognition 
    184         # Create an if structure to extract the params from a match if it occurs. 
    185         body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams" 
    186         body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend" 
    187      
    188         # Build the method declaration and compile it 
    189         method_decl = "def recognize(path, env={})\n#{body}\nend" 
    190         instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" 
    191         method_decl 
    192       end 
    193  
    194       # Plugins may override this method to add other conditions, like checks on 
    195       # host, subdomain, and so forth. Note that changes here only affect route 
    196       # recognition, not generation. 
    197       def recognition_conditions 
    198         result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"] 
    199         result << "conditions[:method] === env[:method]" if conditions[:method] 
    200         result 
    201       end 
    202  
    203       # Build the regular expression pattern that will match this route. 
    204       def recognition_pattern(wrap = true) 
    205         pattern = '' 
    206         segments.reverse_each do |segment| 
    207           pattern = segment.build_pattern pattern 
    208         end 
    209         wrap ? ("\\A" + pattern + "\\Z") : pattern 
    210       end 
    211        
    212       # Write the code to extract the parameters from a matched route. 
    213       def recognition_extraction 
    214         next_capture = 1 
    215         extraction = segments.collect do |segment| 
    216           x = segment.match_extraction next_capture 
    217           next_capture += Regexp.new(segment.regexp_chunk).number_of_captures 
    218           x 
    219         end 
    220         extraction.compact 
    221       end 
    222    
    223       # Write the real generation implementation and then resend the message. 
    224       def generate(options, hash, expire_on = {}) 
    225         write_generation 
    226         generate options, hash, expire_on 
    227       end 
    228  
    229       def generate_extras(options, hash, expire_on = {}) 
    230         write_generation 
    231         generate_extras options, hash, expire_on 
    232       end 
    233  
    234       # Generate the query string with any extra keys in the hash and append 
    235       # it to the given path, returning the new path. 
    236       def append_query_string(path, hash, query_keys=nil) 
    237         return nil unless path 
    238         query_keys ||= extra_keys(hash) 
    239         "#{path}#{build_query_string(hash, query_keys)}" 
    240       end 
    241  
    242       # Determine which keys in the given hash are "extra". Extra keys are 
    243       # those that were not used to generate a particular route. The extra 
    244       # keys also do not include those recalled from the prior request, nor 
    245       # do they include any keys that were implied in the route (like a 
    246       # :controller that is required, but not explicitly used in the text of 
    247       # the route.) 
    248       def extra_keys(hash, recall={}) 
    249         (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys 
    250       end 
    251  
    252       # Build a query string from the keys of the given hash. If +only_keys+ 
    253       # is given (as an array), only the keys indicated will be used to build 
    254       # the query string. The query string will correctly build array parameter 
    255       # values. 
    256       def build_query_string(hash, only_keys=nil) 
    257         elements = [] 
    258  
    259         only_keys ||= hash.keys 
    260          
    261         only_keys.each do |key| 
    262           value = hash[key] or next 
    263           key = CGI.escape key.to_s 
    264           if value.class == Array 
    265             key <<  '[]' 
    266           else     
    267             value = [ value ]  
    268           end      
    269           value.each { |val| elements << "#{key}=#{CGI.escape(val.to_param.to_s)}" } 
    270         end      
    271          
    272         query_string = "?#{elements.join("&")}" unless elements.empty? 
    273         query_string || "" 
    274       end 
    275    
    276       # Write the real recognition implementation and then resend the message. 
    277       def recognize(path, environment={}) 
    278         write_recognition 
    279         recognize path, environment 
    280       end 
    281    
    282       # A route's parameter shell contains parameter values that are not in the 
    283       # route's path, but should be placed in the recognized hash. 
    284       #  
    285       # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route: 
    286       #  
    287       #   map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ 
    288       #  
    289       def parameter_shell 
    290         @parameter_shell ||= returning({}) do |shell| 
    291           requirements.each do |key, requirement| 
    292             shell[key] = requirement unless requirement.is_a? Regexp 
    293           end 
    294         end 
    295       end 
    296    
    297       # Return an array containing all the keys that are used in this route. This 
    298       # includes keys that appear inside the path, and keys that have requirements 
    299       # placed upon them. 
    300       def significant_keys 
    301         @significant_keys ||= returning [] do |sk| 
    302           segments.each { |segment| sk << segment.key if segment.respond_to? :key } 
    303           sk.concat requirements.keys 
    304           sk.uniq! 
    305         end 
    306       end 
    307  
    308       # Return a hash of key/value pairs representing the keys in the route that 
    309       # have defaults, or which are specified by non-regexp requirements. 
    310       def defaults 
    311         @defaults ||= returning({}) do |hash| 
    312           segments.each do |segment| 
    313             next unless segment.respond_to? :default 
    314             hash[segment.key] = segment.default unless segment.default.nil? 
    315           end 
    316           requirements.each do |key,req| 
    317             next if Regexp === req || req.nil? 
    318             hash[key] = req 
    319           end 
    320         end 
    321       end 
    322    
    323       def matches_controller_and_action?(controller, action) 
    324         unless @matching_prepared 
    325           @controller_requirement = requirement_for(:controller) 
    326           @action_requirement = requirement_for(:action) 
    327           @matching_prepared = true 
    328         end 
    329  
    330         (@controller_requirement.nil? || @controller_requirement === controller) && 
    331         (@action_requirement.nil? || @action_requirement === action) 
    332       end 
    333  
    334       def to_s 
    335         @to_s ||= begin 
    336           segs = segments.inject("") { |str,s| str << s.to_s } 
    337           "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect] 
    338         end 
    339       end 
    340    
    341     protected 
    342       def requirement_for(key) 
    343         return requirements[key] if requirements.key? key 
    344         segments.each do |segment| 
    345           return segment.regexp if segment.respond_to?(:key) && segment.key == key 
    346         end 
    347         nil 
    348       end 
    349    
    350     end 
    351  
    352     class Segment 
    353       attr_accessor :is_optional 
    354       alias_method :optional?, :is_optional 
    355  
    356       def initialize 
    357         self.is_optional = false 
    358       end 
    359  
    360       def extraction_code 
    361         nil 
    362       end 
    363    
    364       # Continue generating string for the prior segments. 
    365       def continue_string_structure(prior_segments) 
    366         if prior_segments.empty? 
    367           interpolation_statement(prior_segments) 
     27            hash.delete k 
     28            keys_to_delete << k 
     29          end 
     30        end 
     31        hash 
     32      end 
     33       
     34      def test_condition(expression, condition) 
     35        case condition 
     36          when String then "(#{expression} == #{condition.inspect})" 
     37          when Regexp then 
     38            condition = Regexp.new("^#{condition.source}$") unless /^\^.*\$$/ =~ condition.source  
     39            "(#{condition.inspect} =~ #{expression})" 
     40          when Array then 
     41            conds = condition.collect do |condition| 
     42              cond = test_condition(expression, condition) 
     43              (cond[0, 1] == '(' && cond[-1, 1] == ')') ? cond : "(#{cond})" 
     44            end 
     45            "(#{conds.join(' || ')})" 
     46          when true then expression 
     47          when nil then "! #{expression}" 
     48          else 
     49            raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil" 
     50        end 
     51      end 
     52    end 
     53 
     54    class Component #:nodoc: 
     55      def dynamic?()  false end 
     56      def optional?() false end 
     57 
     58      def key() nil end 
     59   
     60      def self.new(string, *args) 
     61        return super(string, *args) unless self == Component 
     62        case string 
     63          when ':controller' then ControllerComponent.new(:controller, *args) 
     64          when /^:(\w+)$/    then DynamicComponent.new($1, *args) 
     65          when /^\*(\w+)$/   then PathComponent.new($1, *args) 
     66          else StaticComponent.new(string, *args) 
     67        end 
     68      end  
     69    end 
     70 
     71    class StaticComponent < Component #:nodoc: 
     72      attr_reader :value 
     73   
     74      def initialize(value) 
     75        @value = value 
     76      end 
     77 
     78      def write_recognition(g) 
     79        g.if_next_matches(value) do |gp| 
     80          gp.move_forward {|gpp| gpp.continue} 
     81        end 
     82      end 
     83 
     84      def write_generation(g) 
     85        g.add_segment(value) {|gp| gp.continue } 
     86      end 
     87    end 
     88 
     89    class DynamicComponent < Component #:nodoc: 
     90      attr_reader :key, :default 
     91      attr_accessor :condition 
     92   
     93      def dynamic?()  true      end 
     94      def optional?() @optional end 
     95 
     96      def default=(default) 
     97        @optional = true 
     98        @default = default 
     99      end 
     100     
     101      def initialize(key, options = {}) 
     102        @key = key.to_sym 
     103        @optional = false 
     104        default, @condition = options[:default], options[:condition] 
     105        self.default = default if options.key?(:default) 
     106      end 
     107 
     108      def default_check(g) 
     109        presence = "#{g.hash_value(key, !! default)}" 
     110        if default 
     111           "!(#{presence} && #{g.hash_value(key, false)} != #{default.to_s.inspect})" 
    368112        else 
    369           new_priors = prior_segments[0..-2] 
    370           prior_segments.last.string_structure(new_priors) 
    371         end 
    372       end 
    373    
    374       # Return a string interpolation statement for this segment and those before it. 
    375       def interpolation_statement(prior_segments) 
    376         chunks = prior_segments.collect { |s| s.interpolation_chunk } 
    377         chunks << interpolation_chunk 
    378         "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}" 
    379       end 
    380    
    381       def string_structure(prior_segments) 
    382         optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments) 
    383       end 
    384    
    385       # Return an if condition that is true if all the prior segments can be generated. 
    386       # If there are no optional segments before this one, then nil is returned. 
    387       def all_optionals_available_condition(prior_segments) 
    388         optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact 
    389         optional_locals.empty? ? nil : " if #{optional_locals * ' && '}" 
    390       end 
    391    
    392       # Recognition 
    393    
    394       def match_extraction(next_capture) 
    395         nil 
    396       end 
    397    
    398       # Warning 
    399    
    400       # Returns true if this segment is optional? because of a default. If so, then 
    401       # no warning will be emitted regarding this segment. 
    402       def optionality_implied? 
    403         false 
    404       end 
    405     end 
    406  
    407     class StaticSegment < Segment 
    408       attr_accessor :value, :raw 
    409       alias_method :raw?, :raw 
    410    
    411       def initialize(value = nil) 
    412         super() 
    413         self.value = value 
    414       end 
    415    
    416       def interpolation_chunk 
    417         raw? ? value : CGI.escape(value) 
    418       end 
    419    
    420       def regexp_chunk 
    421         chunk = Regexp.escape value 
    422         optional? ? Regexp.optionalize(chunk) : chunk 
    423       end 
    424    
    425       def build_pattern(pattern) 
    426         escaped = Regexp.escape(value) 
    427         if optional? && ! pattern.empty? 
    428           "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})" 
    429         elsif optional? 
    430           Regexp.optionalize escaped 
    431         else 
    432           escaped + pattern 
    433         end 
    434       end 
    435    
    436       def to_s 
    437         value 
    438       end 
    439     end 
    440  
    441     class DividerSegment < StaticSegment 
    442       def initialize(value = nil) 
    443         super(value) 
    444         self.raw = true 
    445         self.is_optional = true 
    446       end 
    447    
    448       def optionality_implied? 
    449         true 
    450       end 
    451     end 
    452  
    453     class DynamicSegment < Segment 
    454       attr_accessor :key, :default, :regexp 
    455    
    456       def initialize(key = nil, options = {}) 
    457         super() 
    458         self.key = key 
    459         self.default = options[:default] if options.key? :default 
    460         self.is_optional = true if options[:optional] || options.key?(:default) 
    461       end 
    462    
    463       def to_s 
    464         ":#{key}" 
    465       end 
    466    
    467       # The local variable name that the value of this segment will be extracted to. 
    468       def local_name 
    469         "#{key}_value" 
    470       end 
    471    
    472       def extract_value 
    473         "#{local_name} = hash[:#{key}] #{"|| #{default.inspect}" if default}" 
    474       end 
    475       def value_check 
    476         if default # Then we know it won't be nil 
    477           "#{value_regexp.inspect} =~ #{local_name}" if regexp 
    478         elsif optional? 
    479           # If we have a regexp check that the value is not given, or that it matches. 
    480           # If we have no regexp, return nil since we do not require a condition. 
    481           "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp 
    482         else # Then it must be present, and if we have a regexp, it must match too. 
    483           "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}" 
    484         end 
    485       end 
    486       def expiry_statement 
    487         "not_expired, hash = false, options if not_expired && expire_on[:#{key}]" 
    488       end 
    489    
    490       def extraction_code 
    491         s = extract_value 
    492         vc = value_check 
    493         s << "\nreturn [nil,nil] unless #{vc}" if vc 
    494         s << "\n#{expiry_statement}" 
    495       end 
    496    
    497       def interpolation_chunk 
    498         "\#{CGI.escape(#{local_name}.to_s)}" 
    499       end 
    500    
    501       def string_structure(prior_segments) 
    502         if optional? # We have a conditional to do... 
    503           # If we should not appear in the url, just write the code for the prior 
    504           # segments. This occurs if our value is the default value, or, if we are 
    505           # optional, if we have nil as our value. 
    506           "if #{local_name} == #{default.inspect}\n" +  
    507             continue_string_structure(prior_segments) +  
    508           "\nelse\n" + # Otherwise, write the code up to here 
    509             "#{interpolation_statement(prior_segments)}\nend" 
    510         else 
    511           interpolation_statement(prior_segments) 
    512         end 
    513       end 
    514    
    515       def value_regexp 
    516         Regexp.new "\\A#{regexp.source}\\Z" if regexp 
    517       end 
    518       def regexp_chunk 
    519         regexp ? "(#{regexp.source})" : "([^#{Routing::SEPARATORS.join}]+)" 
    520       end 
    521    
    522       def build_pattern(pattern) 
    523         chunk = regexp_chunk 
    524         chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0 
    525         pattern = "#{chunk}#{pattern}" 
    526         optional? ? Regexp.optionalize(pattern) : pattern 
    527       end 
    528       def match_extraction(next_capture) 
    529         hangon = (default ? "|| #{default.inspect}" : "if match[#{next_capture}]") 
    530          
    531         # All non code-related keys (such as :id, :slug) have to be unescaped as other CGI params 
    532         "params[:#{key}] = match[#{next_capture}] #{hangon}" 
    533       end 
    534    
    535       def optionality_implied? 
    536         [:action, :id].include? key 
    537       end 
    538    
    539     end 
    540  
    541     class ControllerSegment < DynamicSegment 
    542       def regexp_chunk 
    543         possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name } 
    544         "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))" 
    545       end 
    546  
    547       # Don't CGI.escape the controller name, since it may have slashes in it, 
    548       # like admin/foo. 
    549       def interpolation_chunk 
    550         "\#{#{local_name}.to_s}" 
    551       end 
    552  
    553       # Make sure controller names like Admin/Content are correctly normalized to 
    554       # admin/content 
    555       def extract_value 
    556         "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase" 
    557       end 
    558  
    559       def match_extraction(next_capture) 
    560         hangon = (default ? "|| #{default.inspect}" : "if match[#{next_capture}]") 
    561         "params[:#{key}] = match[#{next_capture}].downcase #{hangon}" 
    562       end 
    563     end 
    564  
    565     class PathSegment < DynamicSegment 
    566       EscapedSlash = CGI.escape("/") 
    567       def interpolation_chunk 
    568         "\#{CGI.escape(#{local_name}.to_s).gsub(#{EscapedSlash.inspect}, '/')}" 
    569       end 
    570  
    571       def default 
    572         '' 
    573       end 
    574  
    575       def default=(path) 
    576         raise RoutingError, "paths cannot have non-empty default values" unless path.blank? 
    577       end 
    578  
    579       def match_extraction(next_capture) 
    580         "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}" 
    581       end 
    582  
    583       def regexp_chunk 
    584         regexp || "(.*)" 
    585       end 
    586  
     113          "! #{presence}" 
     114        end 
     115      end 
     116   
     117      def write_generation(g) 
     118        wrote_dropout = write_dropout_generation(g) 
     119        write_continue_generation(g, wrote_dropout) 
     120      end 
     121 
     122      def write_dropout_generation(g) 
     123        return false unless optional? && g.after.all? {|c| c.optional?} 
     124     
     125        check = [default_check(g)] 
     126        gp = g.dup # Use another generator to write the conditions after the first && 
     127        # We do this to ensure that the generator will not assume x_value is set. It will 
     128        # not be set if it follows a false condition -- for example, false && (x = 2) 
     129         
     130        check += gp.after.map {|c| c.default_check gp} 
     131        gp.if(check.join(' && ')) { gp.finish } # If this condition is met, we stop here 
     132        true  
     133      end 
     134 
     135      def write_continue_generation(g, use_else) 
     136        test  = Routing.test_condition(g.hash_value(key, true, default), condition || true) 
     137        check = (use_else && condition.nil? && default) ? [:else] : [use_else ? :elsif : :if, test] 
     138     
     139        g.send(*check) do |gp| 
     140          gp.expire_for_keys(key) unless gp.after.empty? 
     141          add_segments_to(gp) {|gpp| gpp.continue} 
     142        end 
     143      end 
     144 
     145      def add_segments_to(g) 
     146        g.add_segment(%(\#{CGI.escape(#{g.hash_value(key, true, default)})})) {|gp| yield gp} 
     147      end 
     148   
     149      def recognition_check(g) 
     150        test_type = [true, nil].include?(condition) ? :presence : :constraint 
     151     
     152        prefix = condition.is_a?(Regexp) ? "#{g.next_segment(true)} && " : '' 
     153        check = prefix + Routing.test_condition(g.next_segment(true), condition || true) 
     154     
     155        g.if(check) {|gp| yield gp, test_type} 
     156      end 
     157   
     158      def write_recognition(g) 
     159        test_type = nil 
     160        recognition_check(g) do |gp, test_type| 
     161          assign_result(gp) {|gpp| gpp.continue} 
     162        end 
     163     
     164        if optional? && g.after.all? {|c| c.optional?} 
     165          call = (test_type == :presence) ? [:else] : [:elsif, "! #{g.next_segment(true)}"] 
     166        
     167          g.send(*call) do |gp| 
     168            assign_default(gp) 
     169            gp.after.each {|c| c.assign_default(gp)} 
     170            gp.finish(false) 
     171          end 
     172        end 
     173      end 
     174 
     175      def assign_result(g, with_default = false) 
     176        g.result key, "CGI.unescape(#{g.next_segment(true, with_default ? default : nil)})" 
     177        g.move_forward {|gp| yield gp} 
     178      end 
     179 
     180      def assign_default(g) 
     181        g.constant_result key, default unless default.nil? 
     182      end 
     183    end 
     184 
     185    class ControllerComponent < DynamicComponent #:nodoc: 
     186      def key() :controller end 
     187 
     188      def add_segments_to(g) 
     189        g.add_segment(%(\#{#{g.hash_value(key, true, default)}})) {|gp| yield gp} 
     190      end 
     191     
     192      def recognition_check(g) 
     193        g << "controller_result = ::ActionController::Routing::ControllerComponent.traverse_to_controller(#{g.path_name}, #{g.index_name})"  
     194        g.if('controller_result') do |gp| 
     195          gp << 'controller_value, segments_to_controller = controller_result' 
     196          if condition 
     197            gp << "controller_path = #{gp.path_name}[#{gp.index_name},segments_to_controller].join('/')" 
     198            gp.if(Routing.test_condition("controller_path", condition)) do |gpp| 
     199              gpp.move_forward('segments_to_controller') {|gppp| yield gppp, :constraint} 
     200            end 
     201          else 
     202            gp.move_forward('segments_to_controller') {|gpp| yield gpp, :constraint} 
     203          end 
     204        end 
     205      end 
     206 
     207      def assign_result(g) 
     208        g.result key, 'controller_value' 
     209        yield g 
     210      end 
     211 
     212      def assign_default(g) 
     213        ControllerComponent.assign_controller(g, default) 
     214      end 
     215   
     216      class << self 
     217        def assign_controller(g, controller) 
     218          expr = "::#{controller.split('/').collect {|c| c.camelize}.join('::')}Controller" 
     219          g.result :controller, expr, true 
     220        end 
     221         
     222        def file_kinds(kind) 
     223          ((@file_kinds ||= []) << kind).uniq! || @file_kinds 
     224        end 
     225         
     226        def traverse_to_controller(segments, start_at = 0) 
     227          mod = ::Object 
     228          length = segments.length 
     229          index = start_at 
     230          mod_name = controller_name = segment = nil 
     231          while index < length 
     232            return nil unless /\A[A-Za-z][A-Za-z\d_]*\Z/ =~ (segment = segments[index]) 
     233            index += 1 
     234             
     235            file_kinds :app 
     236            mod_name = segment.camelize 
     237            controller_name = "#{mod_name}Controller" 
     238            path_suffix = File.join(segments[start_at..(index - 1)]) 
     239            next_mod = nil 
     240             
     241            # If the controller is already present, or if we load it, return it. 
     242            if mod.const_defined?(controller_name) || attempt_load(mod, controller_name, path_suffix + "_controller") == :defined 
     243              controller = mod.const_get(controller_name) 
     244              return nil unless controller.is_a?(Class) && controller.ancestors.include?(ActionController::Base) # it's not really a controller? 
     245              return [controller, (index - start_at)] 
     246            end 
     247             
     248            # No controller? Look for the module 
     249            if mod.const_defined? mod_name 
     250              next_mod = mod.send(:const_get, mod_name) 
     251              next_mod = nil unless next_mod.is_a?(Module) 
     252            else 
     253              # Try to load a file that defines the module we want. 
     254              case attempt_load(mod, mod_name, path_suffix) 
     255                when :defined then next_mod = mod.const_get mod_name 
     256                when :dir then # We didn't find a file, but there's a dir. 
     257                  next_mod = Module.new # So create a module for the directory 
     258                  mod.send :const_set, mod_name, next_mod 
     259                else 
     260                  return nil 
     261              end 
     262            end 
     263            mod = next_mod 
     264             
     265            return nil unless mod && mod.is_a?(Module) 
     266          end 
     267          nil 
     268        end 
     269         
     270      protected 
     271        def safe_load_paths #:nodoc: 
     272          if defined?(RAILS_ROOT) 
     273            $LOAD_PATH.select do |base| 
     274              base = File.expand_path(base) 
     275              extended_root = File.expand_path(RAILS_ROOT) 
     276              base.match(/\A#{Regexp.escape(extended_root)}\/*#{file_kinds(:lib) * '|'}/) || base =~ %r{rails-[\d.]+/builtin} 
     277            end 
     278          else 
     279            $LOAD_PATH 
     280          end 
     281        end 
     282         
     283        def attempt_load(mod, const_name, path) 
     284          has_dir = false 
     285          safe_load_paths.each do |load_path| 
     286            full_path = File.join(load_path, path) 
     287            file_path = full_path + '.rb' 
     288            if File.file?(file_path) # Found a .rb file? Load it up 
     289              require_dependency(file_path) 
     290              return :defined if mod.const_defined? const_name 
     291            else 
     292              has_dir ||= File.directory?(full_path) 
     293            end 
     294          end 
     295          return (has_dir ? :dir : nil) 
     296        end 
     297      end 
     298    end 
     299 
     300    class PathComponent < DynamicComponent #:nodoc: 
     301      def optional?() true end 
     302      def default()   []  end 
     303      def condition() nil  end 
     304 
     305      def default=(value) 
     306        raise RoutingError, "All path components have an implicit default of []" unless value == [] 
     307      end 
     308   
     309      def write_generation(g) 
     310        raise RoutingError, 'Path components must occur last' unless g.after.empty? 
     311        g.if("#{g.hash_value(key, true)} && ! #{g.hash_value(key, true)}.empty?") do 
     312          g << "#{g.hash_value(key, true)} = #{g.hash_value(key, true)}.join('/') unless #{g.hash_value(key, true)}.is_a?(String)" 
     313          g.add_segment("\#{CGI.escape_skipping_slashes(#{g.hash_value(key, true)})}") {|gp| gp.finish } 
     314        end 
     315        g.else { g.finish } 
     316      end 
     317   
     318      def write_recognition(g) 
     319        raise RoutingError, "Path components must occur last" unless g.after.empty? 
     320     
     321        start = g.index_name 
     322        start = "(#{start})" unless /^\w+$/ =~ start 
     323     
     324        value_expr = "#{g.path_name}[#{start}..-1] || []" 
     325        g.result key, "ActionController::Routing::PathComponent::Result.new_escaped(#{value_expr})" 
     326        g.finish(false) 
     327      end 
     328   
    587329      class Result < ::Array #:nodoc: 
    588         def to_s() join '/' end  
     330        def to_s() join '/' end 
    589331        def self.new_escaped(strings) 
    590332          new strings.collect {|str| CGI.unescape str} 
    591         end      
    592       end      
    593     end 
    594  
    595     class RouteBuilder 
    596       attr_accessor :separators, :optional_separators 
    597    
     333        end 
     334      end 
     335    end 
     336 
     337    class Route #:nodoc: 
     338      attr_accessor :components, :known 
     339      attr_reader :path, :options, :keys, :defaults 
     340   
     341      def initialize(path, options = {}) 
     342        @path, @options = path, options 
     343     
     344        initialize_components path 
     345        defaults, conditions = initialize_hashes options.dup 
     346        @defaults = defaults.dup 
     347        configure_components(defaults, conditions) 
     348        add_default_requirements 
     349        initialize_keys 
     350      end 
     351   
     352      def inspect 
     353        "<#{self.class} #{path.inspect}, #{options.inspect[1..-1]}>" 
     354      end 
     355   
     356      def write_generation(generator = CodeGeneration::GenerationGenerator.new) 
     357        generator.before, generator.current, generator.after = [], components.first, (components[1..-1] || []) 
     358 
     359        if known.empty? then generator.go 
     360        else 
     361          # Alter the conditions to allow :action => 'index' to also catch :action => nil 
     362          altered_known = known.collect do |k, v| 
     363            if k == :action && v== 'index' then [k, [nil, 'index']] 
     364            else [k, v] 
     365            end 
     366          end 
     367          generator.if(generator.check_conditions(altered_known)) {|gp| gp.go } 
     368        end 
     369         
     370        generator 
     371      end 
     372   
     373      def write_recognition(generator = CodeGeneration::RecognitionGenerator.new) 
     374        g = generator.dup 
     375        g.share_locals_with generator 
     376        g.before, g.current, g.after = [], components.first, (components[1..-1] || []) 
     377     
     378        known.each do |key, value| 
     379          if key == :controller then ControllerComponent.assign_controller(g, value) 
     380          else g.constant_result(key, value) 
     381          end 
     382        end 
     383     
     384        g.go 
     385     
     386        generator 
     387      end 
     388 
     389      def initialize_keys 
     390        @keys = (components.collect {|c| c.key} + known.keys).compact 
     391        @keys.freeze 
     392      end 
     393   
     394      def extra_keys(options) 
     395        options.keys - @keys 
     396      end 
     397     
     398      def matches_controller?(controller) 
     399        if known[:controller] then known[:controller] == controller 
     400        else 
     401          c = components.find {|c| c.key == :controller} 
     402          return false unless c 
     403          return c.condition.nil? || eval(Routing.test_condition('controller', c.condition)) 
     404        end 
     405      end 
     406   
     407      protected 
     408        def initialize_components(path) 
     409          path = path.split('/') if path.is_a? String 
     410          path.shift if path.first.blank? 
     411          self.components = path.collect {|str| Component.new str} 
     412        end 
     413     
     414        def initialize_hashes(options) 
     415          path_keys = components.collect {|c| c.key }.compact  
     416          self.known = {} 
     417          defaults = options.delete(:defaults) || {} 
     418          conditions = options.delete(:require) || {} 
     419          conditions.update(options.delete(:requirements) || {}) 
     420       
     421          options.each do |k, v| 
     422            if path_keys.include?(k) then (v.is_a?(Regexp) ? conditions : defaults)[k] = v 
     423            else known[k] = v 
     424            end 
     425          end 
     426          [defaults, conditions] 
     427        end 
     428     
     429        def configure_components(defaults, conditions) 
     430          components.each do |component| 
     431            if defaults.key?(component.key) then component.default = defaults[component.key] 
     432            elsif component.key == :action  then component.default = 'index' 
     433            elsif component.key == :id      then component.default = nil 
     434            end 
     435         
     436            component.condition = conditions[component.key] if conditions.key?(component.key) 
     437          end 
     438        end 
     439         
     440        def add_default_requirements 
     441          component_keys = components.collect {|c| c.key} 
     442          known[:action] ||= 'index' unless component_keys.include? :action 
     443        end 
     444    end 
     445 
     446    class RouteSet #:nodoc: 
     447      attr_reader :routes, :categories, :controller_to_selector 
    598448      def initialize 
    599         self.separators = Routing::SEPARATORS 
    600         self.optional_separators = %w( / ) 
    601       end 
    602    
    603       def separator_pattern(inverted = false) 
    604         "[#{'^' if inverted}#{Regexp.escape(separators.join)}]" 
    605       end 
    606    
    607       def interval_regexp 
    608         Regexp.new "(.*?)(#{separators.source}|$)" 
    609       end 
    610    
    611       # Accepts a "route path" (a string defining a route), and returns the array 
    612       # of segments that corresponds to it. Note that the segment array is only 
    613       # partially initialized--the defaults and requirements, for instance, need 
    614       # to be set separately, via the #assign_route_options method, and the 
    615       # #optional? method for each segment will not be reliable until after 
    616       # #assign_route_options is called, as well. 
    617       def segments_for_route_path(path) 
    618         rest, segments = path, [] 
    619      
    620         until rest.empty? 
    621           segment, rest = segment_for rest 
    622           segments << segment 
    623         end 
    624         segments 
    625       end 
    626  
    627       # A factory method that returns a new segment instance appropriate for the 
    628       # format of the given string. 
    629       def segment_for(string) 
    630         segment = case string 
    631           when /\A:(\w+)/ 
    632             key = $1.to_sym 
    633             case key 
    634               when :action then DynamicSegment.new(key, :default => 'index') 
    635               when :id then DynamicSegment.new(key, :optional => true) 
    636               when :controller then ControllerSegment.new(key) 
    637               else DynamicSegment.new key 
    638             end 
    639           when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true) 
    640           when /\A\?(.*?)\?/ 
    641             returning segment = StaticSegment.new($1) do 
    642               segment.is_optional = true 
    643             end 
    644           when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1) 
    645           when Regexp.new(separator_pattern) then 
    646             returning segment = DividerSegment.new($&) do 
    647               segment.is_optional = (optional_separators.include? $&) 
    648             end 
    649         end 
    650         [segment, $~.post_match] 
    651       end 
    652    
    653       # Split the given hash of options into requirement and default hashes. The 
    654       # segments are passed alongside in order to distinguish between default values 
    655       # and requirements. 
    656       def divide_route_options(segments, options) 
    657         options = options.dup 
    658         requirements = (options.delete(:requirements) || {}).dup 
    659         defaults     = (options.delete(:defaults)     || {}).dup 
    660         conditions   = (options.delete(:conditions)   || {}).dup 
    661  
    662         path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact 
    663         options.each do |key, value| 
    664           hash = (path_keys.include?(key) && ! value.is_a?(Rege