Changeset 429

Show
Ignore:
Timestamp:
08/26/07 22:29:41 (1 year ago)
Author:
duane.johns..@gmail.com
Message:

Added flexible routes system. Closes ticket #131. [Duane Johnson]

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/History.txt

    r419 r429  
    55* Fixed bug where image_tag would prepend /images/ to urls starting with http:// or https://. 
    66* benchmarking added to exceptions. 
     7* Added a more flexible routes system that allows routing based on any request 
     8  environment variable.  Regular expressions now form base of all routes. 
    79 
    810See also: 
  • trunk/Rakefile

    r427 r429  
    123123end 
    124124 
     125desc "Run a specific spec with TASK=xxxx" 
     126Spec::Rake::SpecTask.new('spec') do |t| 
     127  t.spec_opts = ["--format", "specdoc", "--colour"] 
     128  t.libs = ['lib', 'server/lib' ] 
     129  t.spec_files = ["specs/merb/merb_#{ENV['TASK']}_spec.rb"] 
     130end 
     131 
    125132desc "Run all specs output html" 
    126133Spec::Rake::SpecTask.new('specs_html') do |t| 
  • trunk/lib/merb/controller.rb

    r401 r429  
    3232      end 
    3333       
    34       def build(req, env, args, resp
     34      def build(status, headers, response, request, params, cookies
    3535        cont = new 
    36         cont.parse_request(req, env, args, resp
     36        cont.set_dispatch_variables(request, status, headers, response, params, cookies
    3737        cont 
    3838      end 
    3939    end 
    40  
    41     # Parses the http request into params, headers and cookies that you can use 
    42     # in your controller classes. Also handles file uploads by writing a 
    43     # tempfile and passing a reference in params. 
    44     def parse_request(req, env, args, resp) 
    45       env = env.to_hash 
    46       @_status, method, @_response, @_headers = 200, (env[Merb::Const::REQUEST_METHOD]||Merb::Const::GET).downcase.to_sym, resp, 
    47          {'Content-Type' =>'text/html'} 
    48       cookies = query_parse(env[Merb::Const::HTTP_COOKIE], ';,') 
    49       querystring = query_parse(env[Merb::Const::QUERY_STRING]) 
    50        
    51       if Merb::Const::MULTIPART_REGEXP =~ env[Merb::Const::UPCASE_CONTENT_TYPE] && [:put,:post].include?(method) 
    52         querystring.update(parse_multipart(req, $1, env)) 
    53       elsif [:post, :put].include?(method) 
    54         if [Merb::Const::APPLICATION_JSON, Merb::Const::TEXT_JSON].include?(env[Merb::Const::UPCASE_CONTENT_TYPE]) 
    55           MERB_LOGGER.info("JSON Request") 
    56           json = JSON.parse(req.read || "") || {} 
    57           querystring.update(json) 
    58         elsif [Merb::Const::APPLICATION_XML, Merb::Const::TEXT_XML].include?(env[Merb::Const::UPCASE_CONTENT_TYPE]) 
    59           querystring.update(Hash.from_xml(req.read).with_indifferent_access) 
    60         else 
    61           querystring.update(query_parse(req.read)) 
    62         end   
    63       end 
    64        
    65       @_cookies, @_params = cookies.symbolize_keys!, querystring.update(args).symbolize_keys! 
    66        
    67       if @_params.key?(_session_id_key) && !Merb::Server.config[:session_id_cookie_only] 
    68         @_cookies[_session_id_key] = @_params[_session_id_key] 
    69       elsif @_params.key?(_session_id_key) && Merb::Server.config[:session_id_cookie_only] 
     40     
     41    def set_dispatch_variables(request, status, headers, response, params, cookies) 
     42      if params.key?(_session_id_key) && !Merb::Server.config[:session_id_cookie_only] 
     43        cookies[_session_id_key] = params[_session_id_key] 
     44      elsif params.key?(_session_id_key) && Merb::Server.config[:session_id_cookie_only] 
    7045        # This condition allows for certain controller/action paths to allow a 
    7146        # session ID to be passed in a query string. This is needed for Flash 
     
    7348        # running session.regenerate after any controller taking advantage of 
    7449        # this in case someone is attempting a session fixation attack 
    75         @_cookies[_session_id_key] = @_params[_session_id_key] if Merb::Server.config[:query_string_whitelist].include?("#{params[:controller]}/#{params[:action]}") 
    76       end   
     50        cookies[_session_id_key] = params[_session_id_key] if Merb::Server.config[:query_string_whitelist].include?("#{params[:controller]}/#{params[:action]}") 
     51      end 
    7752       
    78       # Handle alternate HTTP method passed as _method parameter. Doesn't allow 
    79       # method to be overridden for :get unless Merb is in development mode. 
    80       #  
    81       #  i.e. You can pass _method=put on the querystring if you are in 
    82       # development mode. 
    83       allow = [:post, :put, :delete] 
    84       allow << :get if MERB_ENV == 'development' 
    85       if @_params.key?(:_method) && allow.include?(method) 
    86         method = @_params.delete(:_method).downcase.intern  
    87       end 
    88       @_request = Request.new(env, method, req) 
    89       MERB_LOGGER.info("Params: #{params.inspect}\nCookies: #{cookies.inspect}") 
     53      @_request, @_status, @_headers, @_response, @_params, @_cookies = \ 
     54        request, status, headers, response, params, cookies 
    9055    end 
    91  
    92    
    93  
     56     
    9457    def dispatch(action=:index) 
    9558      start = Time.now 
  • trunk/lib/merb/core_ext.rb

    r401 r429  
    1 corelib = __DIR__+'/merb/core_ext' 
     1corelib = File.dirname(__FILE__)+'/core_ext' 
    22 
    33%w[ inflector 
  • trunk/lib/merb/core_ext/string.rb

    r400 r429  
    1616  end 
    1717   
     18  # Takes lines of text, removes any indentation, and 
     19  # adds +indentation+ number of spaces to each line 
     20  def indent(indentation) 
     21    lines = to_a 
     22    initial_indentation = lines.first.scan(/^(\s+)/).flatten.first 
     23    lines.map do |line| 
     24      if initial_indentation.nil? 
     25        " " * indentation + line 
     26      elsif line.index(initial_indentation) == 0 
     27        " " * indentation + line[initial_indentation.size..-1] 
     28      else 
     29        " " * indentation + line 
     30      end 
     31    end.join 
     32  end 
     33   
    1834end 
  • trunk/lib/merb/dispatcher.rb

    r412 r429  
    2222          start = Time.now 
    2323           
    24           request_uri = request.params[Merb::Const::REQUEST_URI] 
    25           request_uri.sub!(path_prefix, '') if path_prefix 
    26           route = route_path(request_uri) 
    27            
    28           allowed = route.delete(:allowed) 
    29           rest = route.delete(:rest) 
    30           namespace = route.delete(:namespace) 
    31            
    32           cont = namespace ? "#{namespace}/#{route[:controller]}" : route[:controller] 
    33            
    34           klass = resolve_controller(cont) 
    35           controller = klass.build(request.body, request.params, route, response) 
    36            
    37           if rest 
    38             method = controller.request.method 
    39             if allowed.keys.include?(method) && action = allowed[method] 
    40               controller.params[:action] = action 
    41             else 
    42               raise Merb::HTTPMethodNotAllowed.new(method, allowed) 
    43             end   
    44           else   
    45             action = route[:action] 
    46           end 
     24          # request_uri = request.params[Merb::Const::REQUEST_URI] 
     25          # request_uri.sub!(path_prefix, '') if path_prefix 
     26          # TODO: Fix path_prefix to work again 
     27           
     28          status, response, headers = 200, request.body, {'Content-Type' =>'text/html'} 
     29          merb_request, params, cookies = parse_request(request.body, request.params) 
     30           
     31          params.merge!(route = Merb::Router.match(merb_request)) 
     32           
     33          klass = resolve_controller(route[:controller]) 
     34          controller = klass.build(status, headers, response, merb_request, params, cookies) 
     35          action = route[:action] 
     36 
    4737          controller._benchmarks[:setup_time] = Time.now - start 
    4838          if @@use_mutex 
    49             @@mutex.synchronize { 
    50               controller.dispatch(action) 
    51             } 
     39            @@mutex.synchronize { controller.dispatch(action) } 
    5240          else 
    5341            controller.dispatch(action) 
     
    6351      end 
    6452       
    65       def route_path(path) 
    66         path = path.sub(/\/+/, '/').sub(/\?.*$/, '') 
    67         path = path[0..-2] if (path[-1] == ?/) && path.size > 1 
    68         Merb::Router.match(path) 
     53      # Parses the http request into params, headers and cookies that you can use 
     54      # in your controller classes. Also handles file uploads by writing a 
     55      # tempfile and passing a reference in params.  Returns the tuple [request, params, cookies] 
     56      def parse_request(request_body, env) 
     57        env = env.to_hash 
     58        method = (env[Merb::Const::REQUEST_METHOD]||Merb::Const::GET).downcase.to_sym 
     59        cookies = query_parse(env[Merb::Const::HTTP_COOKIE], ';,') 
     60        querystring = query_parse(env[Merb::Const::QUERY_STRING]) 
     61 
     62        if Merb::Const::MULTIPART_REGEXP =~ env[Merb::Const::UPCASE_CONTENT_TYPE] && [:put,:post].include?(method) 
     63          querystring.update(parse_multipart(request_body, $1, env)) 
     64        elsif [:post, :put].include?(method) 
     65          if [Merb::Const::APPLICATION_JSON, Merb::Const::TEXT_JSON].include?(env[Merb::Const::UPCASE_CONTENT_TYPE]) 
     66            MERB_LOGGER.info("JSON Request") 
     67            json = JSON.parse(request_body.read || "") || {} 
     68            querystring.update(json) 
     69          elsif [Merb::Const::APPLICATION_XML, Merb::Const::TEXT_XML].include?(env[Merb::Const::UPCASE_CONTENT_TYPE]) 
     70            querystring.update(Hash.from_xml(request_body.read).with_indifferent_access) 
     71          else 
     72            querystring.update(query_parse(request_body.read)) 
     73          end   
     74        end 
     75 
     76        cookies, params = cookies.symbolize_keys!, querystring.symbolize_keys! 
     77 
     78        # Handle alternate HTTP method passed as _method parameter. Doesn't allow 
     79        # method to be overridden for :get unless Merb is in development mode. 
     80        #  
     81        #  i.e. You can pass _method=put on the querystring if you are in 
     82        # development mode. 
     83        allow = [:post, :put, :delete] 
     84        allow << :get if MERB_ENV == 'development' 
     85        if params.key?(:_method) && allow.include?(method) 
     86          method = params.delete(:_method).downcase.intern  
     87        end 
     88        request = Request.new(env, method, request_body) 
     89        MERB_LOGGER.info("Params: #{params.inspect}\nCookies: #{cookies.inspect}") 
     90        [request, params, cookies] 
    6991      end 
    7092       
     
    87109      end 
    88110 
     111      NAME_REGEX = /Content-Disposition:.* name="?([^\";]*)"?/ni.freeze 
     112      CONTENT_TYPE_REGEX = /Content-Type: (.*)\r\n/ni.freeze 
     113      FILENAME_REGEX = /Content-Disposition:.* filename="?([^\";]*)"?/ni.freeze 
     114      CRLF = "\r\n".freeze 
     115      EOL = CRLF 
     116      def parse_multipart(request,boundary,env) 
     117        boundary = "--#{boundary}" 
     118        paramhsh = {} 
     119        buf = "" 
     120        content_length = env['CONTENT_LENGTH'].to_i 
     121        input = request 
     122        input.binmode if defined? input.binmode 
     123        boundary_size = boundary.size + EOL.size 
     124        bufsize = 16384 
     125        content_length -= boundary_size 
     126        status = input.read(boundary_size) 
     127        raise EOFError, "bad content body"  unless status == boundary + EOL 
     128        rx = /(?:#{EOL})?#{Regexp.quote(boundary,'n')}(#{EOL}|--)/ 
     129        loop { 
     130          head = nil 
     131          body = '' 
     132          filename = content_type = name = nil 
     133          read_size = 0 
     134          until head && buf =~ rx 
     135            i = buf.index("\r\n\r\n") 
     136            if( i == nil && read_size == 0 && content_length == 0 ) 
     137              content_length = -1 
     138              break 
     139            end 
     140            if !head && i 
     141              head = buf.slice!(0, i+2) # First \r\n 
     142              buf.slice!(0, 2)          # Second \r\n 
     143              filename = head[FILENAME_REGEX, 1] 
     144              content_type = head[CONTENT_TYPE_REGEX, 1] 
     145              name = head[NAME_REGEX, 1] 
     146 
     147              if filename && !filename.empty?     
     148                body = Tempfile.new(:Merb) 
     149                body.binmode if defined? body.binmode 
     150              end 
     151              next 
     152            end 
     153 
     154 
     155            # Save the read body part. 
     156            if head && (boundary_size+4 < buf.size) 
     157              body << buf.slice!(0, buf.size - (boundary_size+4)) 
     158            end 
     159 
     160            read_size = bufsize < content_length ? bufsize : content_length 
     161            if( read_size > 0 ) 
     162              c = input.read(read_size) 
     163              raise EOFError, "bad content body"  if c.nil? || c.empty? 
     164              buf << c 
     165              content_length -= c.size 
     166            end 
     167          end 
     168 
     169          # Save the rest. 
     170          if i = buf.index(rx) 
     171            body << buf.slice!(0, i) 
     172            buf.slice!(0, boundary_size+2) 
     173 
     174            content_length = -1  if $1 == "--" 
     175          end 
     176 
     177          if filename && !filename.empty?    
     178            body.rewind 
     179            data = {  
     180                  :filename => File.basename(filename),   
     181                  :content_type => content_type,   
     182                  :tempfile => body,  
     183                  :size => File.size(body)  
     184                } 
     185          else 
     186            data = body 
     187          end 
     188          paramhsh = normalize_params(paramhsh,name,data) 
     189          break  if buf.empty? || content_length == -1 
     190        } 
     191        paramhsh 
     192      end 
     193 
     194      def normalize_params(parms, key, val) 
     195        key =~ /\]?\[/ 
     196        before = $` 
     197        after = $' 
     198 
     199        if before.nil? 
     200          parms[key.sub("]", "")] = val 
     201        elsif after == "]" 
     202          (parms[before] ||= []) << val 
     203        else 
     204          parms[before] ||= {} 
     205          parms[before] = normalize_params(parms[before], after, val) 
     206        end 
     207        parms 
     208      end 
     209 
     210      # parses a query string or the payload of a POST 
     211      # request into the params hash. So for example: 
     212      # /foo?bar=nik&post[title]=heya&post[body]=whatever 
     213      # parses into: 
     214      # {:bar => 'nik', :post => {:title => 'heya', :body => 'whatever'}} 
     215      def query_parse(qs, d = '&;') 
     216        (qs||'').split(/[#{d}] */n).inject({}) { |h,p|  
     217          normalize_params(h, *Mongrel::HttpRequest.unescape(p).split('=',2)) 
     218        } 
     219      end 
     220       
    89221    end # end class << self 
    90222       
  • trunk/lib/merb/mixins/controller.rb

    r425 r429  
    4545     
    4646    protected 
    47     NAME_REGEX = /Content-Disposition:.* name="?([^\";]*)"?/ni.freeze 
    48     CONTENT_TYPE_REGEX = /Content-Type: (.*)\r\n/ni.freeze 
    49     FILENAME_REGEX = /Content-Disposition:.* filename="?([^\";]*)"?/ni.freeze 
    50     CRLF = "\r\n".freeze 
    51     EOL = CRLF 
    52     def parse_multipart(request,boundary,env) 
    53       boundary = "--#{boundary}" 
    54       paramhsh = {} 
    55       buf = "" 
    56       content_length = env['CONTENT_LENGTH'].to_i 
    57       input = request 
    58       input.binmode if defined? input.binmode 
    59       boundary_size = boundary.size + EOL.size 
    60       bufsize = 16384 
    61       content_length -= boundary_size 
    62       status = input.read(boundary_size) 
    63       raise EOFError, "bad content body"  unless status == boundary + EOL 
    64       rx = /(?:#{EOL})?#{Regexp.quote(boundary,'n')}(#{EOL}|--)/ 
    65       loop { 
    66         head = nil 
    67         body = '' 
    68         filename = content_type = name = nil 
    69         read_size = 0 
    70         until head && buf =~ rx 
    71           i = buf.index("\r\n\r\n") 
    72           if( i == nil && read_size == 0 && content_length == 0 ) 
    73             content_length = -1 
    74             break 
    75           end 
    76           if !head && i 
    77             head = buf.slice!(0, i+2) # First \r\n 
    78             buf.slice!(0, 2)          # Second \r\n 
    79             filename = head[FILENAME_REGEX, 1] 
    80             content_type = head[CONTENT_TYPE_REGEX, 1] 
    81             name = head[NAME_REGEX, 1] 
    82              
    83             if filename && !filename.empty?     
    84               body = Tempfile.new(:Merb) 
    85               body.binmode if defined? body.binmode 
    86             end 
    87             next 
    88           end 
    89  
    90            
    91           # Save the read body part. 
    92           if head && (boundary_size+4 < buf.size) 
    93             body << buf.slice!(0, buf.size - (boundary_size+4)) 
    94           end 
    95        
    96           read_size = bufsize < content_length ? bufsize : content_length 
    97           if( read_size > 0 ) 
    98             c = input.read(read_size) 
    99             raise EOFError, "bad content body"  if c.nil? || c.empty? 
    100             buf << c 
    101             content_length -= c.size 
    102           end 
    103         end 
    104  
    105         # Save the rest. 
    106         if i = buf.index(rx) 
    107           body << buf.slice!(0, i) 
    108           buf.slice!(0, boundary_size+2) 
    109        
    110           content_length = -1  if $1 == "--" 
    111         end 
    112          
    113         if filename && !filename.empty?    
    114           body.rewind 
    115           data = {  
    116                   :filename => File.basename(filename),   
    117                   :content_type => content_type,   
    118                   :tempfile => body,  
    119                   :size => File.size(body)  
    120                 } 
    121         else 
    122           data = body 
    123         end 
    124         paramhsh = normalize_params(paramhsh,name,data) 
    125         break  if buf.empty? || content_length == -1 
    126       } 
    127       paramhsh 
    128     end 
    129      
    130     def normalize_params(parms, key, val) 
    131       key =~ /\]?\[/ 
    132       before = $` 
    133       after = $' 
    134        
    135       if before.nil? 
    136         parms[key.sub("]", "")] = val 
    137       elsif after == "]" 
    138         (parms[before] ||= []) << val 
    139       else 
    140         parms[before] ||= {} 
    141         parms[before] = normalize_params(parms[before], after, val) 
    142       end 
    143       parms 
    144     end 
    145      
    146     # parses a query string or the payload of a POST 
    147     # request into the params hash. So for example: 
    148     # /foo?bar=nik&post[title]=heya&post[body]=whatever 
    149     # parses into: 
    150     # {:bar => 'nik', :post => {:title => 'heya', :body => 'whatever'}} 
    151     def query_parse(qs, d = '&;') 
    152       (qs||'').split(/[#{d}] */n).inject({}) { |h,p|  
    153         normalize_params(h, *unescape(p).split('=',2)) 
    154       } 
    155     end 
    156  
    15747    # render using chunked encoding 
    15848    # def stream 
  • trunk/lib/merb/request.rb

    r399 r429  
    119119    end 
    120120     
    121     # returns the uri without the query string. 
     121    # Returns the uri without the query string. Strips trailing '/' and reduces 
     122    # duplicate '/' to a single '/' 
    122123    def path 
    123       uri ? uri.split('?').first : '' 
     124      path = (uri ? uri.split('?').first : '').sub(/\/+/, '/') 
     125      path = path[0..-2] if (path[-1] == ?/) && path.size > 1 
     126      path 
    124127    end 
    125128     
  • trunk/lib/merb/router.rb

    r399 r429  
    22 
    33  class Router 
     4    SEGMENT_REGEXP = /(:[a-z*_]+)/.freeze 
     5    LOOKAHEAD_SEGMENT_REGEXP = /(?::([a-z*_]+))/.freeze 
     6    PARENTHETICAL_SEGMENT_STRING = "([^\/.,;?]+)".freeze 
    47     
    5     SECTION_REGEXP = /(?::([a-z*_]+))/.freeze 
    6      
     8    URL_PARTS = [ 
     9      :protocol, :domain, :port, :path, :method, :query_string, :accept, 
     10      :path_info, :connection, :gateway, :version, :remote_ip, 
     11      :accept_charset, :accept_encoding, :accept_language, 
     12      :server_software, :server_name, :user_agent, :referer ] 
     13 
     14    class MockRequest 
     15      attr_accessor *URL_PARTS 
     16      def initialize(hash) hash.each_pair { |k, v| instance_variable_set("@#{k}", v) } end 
     17    end 
     18 
     19    module Options 
     20      attr_accessor :options 
     21       
     22      def options(opts = {}) 
     23        @options ||= {} 
     24        if block_given? 
     25          options_push 
     26          options_merge(opts) 
     27          yield self  
     28          options_pop 
     29        else 
     30          @options 
     31        end 
     32      end 
     33       
     34      private 
     35       
     36      def options_push 
     37        @options_stack ||= [] 
     38        @options_stack.push @options 
     39        @options = @options.dup 
     40      end 
     41       
     42      def options_pop 
     43        @options = @options_stack.pop 
     44      end 
     45       
     46      def options_merge(opts) 
     47        opts[:namespace] = (@options[:namespace] || "") + opts[:namespace] if opts[:namespace] 
     48        @options.merge!(opts) 
     49      end 
     50    end 
     51       
    752    class << self 
     53      include Merb::Router::Options 
    854       
    955      def prepare 
     
    1561        @@matcher.compile_router 
    1662      end 
    17        
    18       def add(*route) 
    19         @@matcher.add(*route) 
     63 
     64      # Add a standard regular expression route 
     65      def route(opts, &block) 
     66        @@matcher.route(options.merge(opts), &block) 
    2067      end 
    2168       
     
    2875      end 
    2976       
    30       def match(path) 
    31         @@matcher.route_request(path) 
     77      def match(request) 
     78        # For backwards compatibility, accept a string 
     79        request = {:path => request, :method => :get} if request.is_a? String 
     80        # For easier testing, allow the request to be a Hash in addition to a Merb::Request object 
     81        request = MockRequest.new(request) if request.is_a? Hash 
     82        @@matcher.route_request(request) 
    3283      end 
    3384       
     
    3687      end 
    3788       
    38       def resources(res, opts={}) 
    39         opts[:prefix] ||= "" 
    40         if block_given? 
    41           procs = [] 
    42           yield Resource.new(res, procs, opts) 
    43           procs.reverse.each &:call 
    44         else 
    45           generate_resources_routes(res,opts) 
    46         end 
    47       end 
    48        
    49       def resource(res, opts={}) 
    50         opts[:prefix] ||= "" 
    51         if block_given? 
    52           procs = [] 
    53           yield Resource.new(res, procs, opts) 
    54           procs.reverse.each &:call 
    55         else 
    56           generate_singleton_routes(res,opts) 
    57         end 
    58       end 
    59        
    6089      def default_routes(*a) 
    6190        @@matcher.default_routes(*a) 
     
    6594        @@matcher.compiled_statement 
    6695      end 
    67        
    68       def compiled_regexen 
    69         @@matcher.compiled_regexen 
    70       end 
    71          
    72       def generate_resources_routes(res, opts) 
    73         [@@matcher,@@generator].each { |r| r.generate_resources_routes(res, opts) } 
    74       end 
    75        
    76       def generate_singleton_routes(res, opts) 
    77         [@@matcher,@@generator].each { |r| r.generate_singleton_routes(res, opts) } 
    78       end 
    79  
    8096    end # class << self 
     97     
     98    # Cache regular expressions or procs for future reference in eval statements 
     99    class CachedCode 
     100      @@index = 0 
     101      @@list = [] 
     102       
     103      attr_accessor :cache, :index 
     104       
     105      def initialize(cache) 
     106        @cache, @index = cache, CachedCode.register(self) 
     107      end 
     108       
     109      # Make each CachedCode object embeddable within a string 
     110      def to_s() "CachedCode[#{@index}].cache" end 
     111       
     112      class << self 
     113        def register(cached_code) 
     114          CachedCode[@@index] = cached_code 
     115          @@index += 1 
     116          return @@index - 1 
     117        end 
     118        def []=(index, cached_code) @@list[index] = cached_code end 
     119        def [](index) @@list[index] end 
     120      end 
     121    end 
     122     
     123    class Route 
     124      # Sequence of segments when reconstructing the URL from a Route 
     125      attr_accessor :segments 
     126      # Regular expressions to be matched against the Request object's methods 
     127      attr_accessor *URL_PARTS 
     128      # Optional variables and optional matcher block 
     129      attr_accessor :mappings, :block 
     130       
     131      def initialize(options, &block) 
     132        compile_regexps(options) 
     133        @mappings = options 
     134        @block = CachedCode.new(block) if block 
     135        @segments = [:protocol, :domain, :path] 
     136      end 
     137       
     138      def compile_regexps(regexen) 
     139        URL_PARTS.each do |re_key| 
     140          re = regexen.delete(re_key) 
     141          if re 
     142            # For convenience, replace occurrences of "~~" with a segment-matching regular expression 
     143            as_string = re.to_s.gsub("~~", Router::PARENTHETICAL_SEGMENT_STRING) 
     144            instance_variable_set("@#{re_key}", CachedCode.new(Regexp.new(as_string))) 
     145          end 
     146        end 
     147      end 
     148 
     149      # Creates an array of conditions based on the possible URL_PARTS, e.g. 
     150      # [("(matches[:protocol] = #{protocol}.match(protocol))" if protocol), 
     151      #  ("(matches[:domain] = #{domain}.match(domain))" if domain), 
     152      # ... etc ... ].compact 
     153      def conditions 
     154        cond = URL_PARTS.map do |part| 
     155          if result = send(part) 
     156            "(matches[:#{part}] = #{result}.match(request.send(:#{part}).to_s))" 
     157          end 
     158        end.compact 
     159        cond << "(block_result = #{block}.call(request))" if block 
     160        cond 
     161      end 
     162 
     163      def compile 
     164        if conditions.empty? 
     165          "" 
     166        else 
     167        <<-RUBY.indent(2) 
     168          if #{conditions.join(' and ')} 
     169            params = mapped_results(#{@mappings.inspect}, matches) 
     170            return block_result ? params.merge(block_result) : params 
     171          end 
     172        RUBY 
     173        end 
     174      end 
     175       
     176    end 
    81177     
    82178    class RouteMatcher 
     
    90186      end 
    91187 
     188      def register_route(route) 
     189        @routes << route 
     190        route 
     191      end 
     192       
    92193      # Add a route to be compiled. 
    93       def add(*route) 
    94         opt = Hash === route.last ? route.pop : {} 
    95         if n = opt[:namespace] 
    96           path = "/#{n}#{route[0]}"  
    97         else 
    98           path = route[0] 
    99         end     
    100         @routes << [path, opt] 
    101       end 
    102        
    103       def raw_add(*route) 
    104         @routes << [route[0], (route[1]||{})] 
     194      def route(options, &block) 
     195        @routes << Route.new(options, &block) 
    105196      end 
    106197       
     
    109200      # that matches wins. 
    110201      def compile_router 
    111         router_lambda = @routes.inject("lambda{|path| \n  sections={}\n  case path\n") { |m,r| 
    112           m << compile(r) 
    113         } << "  else\n    return {:controller=>'Noroutefound', :action=>'noroute'}\n  end\n}" 
    114         @compiled_statement = router_lambda 
    115         meta_def(:route_request, &eval(router_lambda)) 
    116       end   
    117        
    118       # Compile each individual route into a when /.../ component of the case 
    119       # statement. Takes /:sections of the route def that start with : and 
    120       # turns them into placeholders for whatever urls match against the route 
    121       # in question. 
    122       def compile(route) 
    123         raise ArgumentError unless String === route[0] 
    124         code, count = '', 0 
    125         while route[0] =~ Router::SECTION_REGEXP 
    126           route[0] = route[0].dup 
     202        header = <<-RUBY.indent(0) 
     203          lambda { |request| 
     204            matches = {} 
     205            block_result = false 
     206        RUBY 
     207        @compiled_statement = @routes.inject(header) { |m, r| m << r.compile } << \ 
     208          "  return {:controller=>'no_route_found', :action=>'index'}\n}" 
     209        meta_def(:route_request, &eval(@compiled_statement)) 
     210      end 
     211       
     212      # Parameters: 
     213      #   mappings - hash containing the user-specified route mappings, e.g. 
     214      #     {:controller => ":path[1]/:path[2]", :action => "index"} 
     215      #   matches - hash containing the MatchData objects against the Merb::Request fields, e.g. 
     216      #     {:path => #<MatchData>, :method => #<MatchData>} 
     217      def mapped_results(mappings, matches) 
     218        params = {} 
     219        mappings.each_pair do |key, map| value = map.dup 
     220          next if key == :namespace 
     221          # Look for place-holder strings, e.g. ":path[4]" or ":protocol[1]" 
     222          map.scan(/:([\w\d]+)\b(\[[\d+]\])?/).uniq.each do |place_holder, index| 
     223            index ||= "[0]" 
     224            integer_index = index[1..-2].to_i # Strip [] and return int value 
     225            literal = ":#{place_holder}#{index}" 
     226            if matches.has_key? place_holder.intern 
     227              value.gsub!(literal, matches[place_holder.intern][integer_index]) 
     228            else 
     229              raise "Route references unknown placeholder '#{place_holder.intern.inspect}': #{mappings.inspect}" 
     230            end 
     231          end 
     232          params[key] = value 
     233        end 
     234        params 
     235      end 
     236 
     237      def default_routes(*a) 
     238      end 
     239    end 
     240 
     241    class RouteGenerator 
     242      attr_accessor :routes 
     243 
     244      def initialize 
     245        @routes = {} 
     246      end 
     247 
     248      def register_route(name, route) 
     249        @routes[name] = route 
     250      end 
     251       
     252      # Take the @segments array in a named route and convert it to a URL by joining 
     253      # string elements and mapped elements (symbols as keys) together. 
     254      def generate(name, mappings = {}) 
     255        raise "No such route '#{name}'" unless @routes.has_key?(name) 
     256        @routes[name].segments.map do |segment| 
     257          param = 
     258            if segment.is_a? Symbol 
     259              if mappings.is_a? Hash 
     260                mappings[segment] 
     261              else 
     262                mappings.send(segment) if mappings.respond_to?(segment) 
     263              end 
     264            elsif segment.respond_to? :to_s 
     265              segment 
     266            else 
     267              raise "Segment type '#{segment.class}' can't be converted to a string" 
     268            end 
     269          (param.respond_to?(:to_param) ? param.to_param : param).to_s 
     270        end.join 
     271      end 
     272    end 
     273 
     274  end 
     275end 
     276 
     277# Modify Router class by adding the simple_route method 
     278module Merb 
     279  class Router 
     280    class << self 
     281      # Add a simplified (non-regular expression) route 
     282      def simple_route(path, mappings = {}, &block) 
     283        raise ArgumentError unless String === path 
     284        opts = {} 
     285        count = 0 
     286        path = "#{mappings[:namespace]}#{path}" 
     287        while path =~ Router::LOOKAHEAD_SEGMENT_REGEXP 
     288          path = path.dup 
     289          # Get the current segment name, e.g. "controller" 
    127290          name = $1 
    128           (name =~ /(\*+)(\w+)/) ? (flag = true; name = $2) : (flag = false) 
    129291          count += 1 
    130           if flag 
    131             route[0].sub!(Router::SECTION_REGEXP, "([^,?]+)") 
    132           else   
    133             route[0].sub!(Router::SECTION_REGEXP, "([^\/,?]+)") 
    134           end 
    135           code << "    sections[:#{name}] = $#{count}\n" 
    136         end 
    137         @compiled_regexen << Regexp.new(route[0]) 
    138         index = @compiled_regexen.size - 1 
    139         condition = "  when @compiled_regexen[#{index}] " 
    140         statement = "#{condition}\n#{code}" 
    141         statement << "    return #{route[1].inspect}.update(sections)\n" 
    142         statement 
    143       end 
    144        
    145       def generate_resources_routes(res,opt) 
    146         with_options opt.merge(:controller => res.to_s, :rest => true) do |r| 
    147           r.raw_add "#{opt[:prefix]}/#{res}/:id[;/]edit", :allowed => {:get => 'edit'} 
    148           r.raw_add "#{opt[:prefix]}/#{res}/new[;/]:action", :allowed => {:get => 'new', :post => 'new', :put => 'new', :delete => 'new'} 
    149           r.raw_add "#{opt[:prefix]}/#{res}/new" , :allowed => {:get => 'new'} 
    150           if mem = opt[:member] 
    151             mem.keys.sort_by{|x| "#{x}"}.each {|action| 
    152               allowed = mem[action].injecting({}) {|h, verb| h[verb] = "#{action}"} 
    153               r.raw_add "#{opt[:prefix]}/#{res}/:id[;/]+#{action}", :allowed => allowed 
    154             } 
    155           end 
    156           if coll = opt[:collection] 
    157             coll.keys.sort_by{|x| "#{x}"}.each {|action| 
    158               allowed = coll[action].injecting({}) {|h, verb| h[verb] = "#{action}"} 
    159               r.raw_add "#{opt[:prefix]}/#{res}[;/]#{action}", :allowed => allowed 
    160             } 
    161           end   
    162           r.raw_add "#{opt[:prefix]}/#{res}/:id\\.:format", :allowed => {:get => 'show', :put => 'update', :delete => 'destroy'} 
    163           r.raw_add "#{opt[:prefix]}/#{res}\\.:format", :allowed => {:get => 'index', :post => 'create'} 
    164           r.raw_add "#{opt[:prefix]}/#{res}/:id", :allowed => {:get => 'show', :put => 'update', :delete => 'destroy'} 
    165           r.raw_add "#{opt[:prefix]}/#{res}/?", :allowed => {:get => 'index', :post => 'create'} 
    166         end 
    167       end 
    168        
    169       def generate_singleton_routes(res,opt) 
    170         with_options opt.merge(:controller => res.to_s, :rest => true ) do |r| 
    171           r.raw_add "#{opt[:prefix]}/#{res}[;/]edit", :allowed => {:get => 'edit'} 
    172           r.raw_add "#{opt[:prefix]}/#{res}\\.:format", :allowed => {:get => 'show'} 
    173           r.raw_add "#{opt[:prefix]}/#{res}/new" , :allowed => {:get => 'new'} 
    174           r.raw_add "#{opt[:prefix]}/#{res}/?", :allowed => {:get => 'show', :post => 'create', :put => 'update', :delete => 'destroy'} 
    175         end 
    176       end 
    177        
    178       def default_routes(opt={}) 
    179         namespace = opt[:namespace] ? "/#{opt[:namespace]}" : "" 
    180         with_options opt do |r| 
    181           r.raw_add namespace + "/:controller/:action/:id\\.:format" 
    182           r.raw_add namespace + "/:controller/:action/:id" 
    183           r.raw_add namespace + "/:controller/:action\\.:format" 
    184           r.raw_add namespace + "/:controller/:action" 
    185           r.raw_add namespace + "/:controller\\.:format", :action => 'index' 
    186           r.raw_add namespace + "/:controller", :action => 'index' 
    187         end   
    188       end 
    189     end 
    190  
    191     class RouteGenerator 
    192  
    193       attr_accessor :paths 
    194  
    195       def initialize 
    196         @paths = {} 
    197       end 
    198  
    199       def add(name, path) 
    200         @paths[name.to_sym] = path 
    201       end 
    202        
    203       def generate(name, *args) 
    204         options = Hash === args.last ? args.pop : {} 
    205         obj = args[0] 
    206         options.each do |key, value| 
    207           next unless value.respond_to?(:to_param) 
    208           unless key.to_s =~ /_?id$/ 
    209             old_key = key 
    210             options[old_key] = value.to_param 
    211             key = "#{key}_id".intern 
    212           end 
    213           options[key] = value.to_param 
    214         end   
    215          
    216         path = @paths[name].dup 
    217         while path =~ Router::SECTION_REGEXP 
    218           if obj.respond_to?($1) && ! obj.nil? 
    219             path.sub!(Router::SECTION_REGEXP, obj.send($1).to_s) 
    220           else   
    221             path.sub!(Router::SECTION_REGEXP, options[$1.intern].to_s) 
    222           end 
    223         end 
    224         if f = options[:format] 
    225           "#{path}.#{f}" 
    226         else 
    227           path 
    228         end    
    229       end 
    230        
    231       def generate_singleton_routes(res,opt) 
    232         res = res.to_s 
    233         if opt[:namespace] 
    234           namespace = "#{opt[:namespace]}_" 
    235           name = "/#{opt[:namespace]}" 
    236         else 
    237           namespace = '' 
    238           name = '' 
    239         end 
    240         add namespace + "edit_#{res}", name + "#{opt[:prefix]}/#{res}/edit" 
    241         add namespace + "new_#{res}", name + "#{opt[:prefix]}/#{res}/new" 
    242         add namespace + res, name + "#{opt[:prefix]}/#{res}" 
    243       end 
    244        
    245       def generate_resources_routes(res,opt) 
     292          # Replace the segment with a proper regular expression 
     293          path.sub!(Router::LOOKAHEAD_SEGMENT_REGEXP, Router::PARENTHETICAL_SEGMENT_STRING) 
     294          # Map the segment to its dynamic value from the request 
     295          opts[name.intern] = ":path[#{count}]" 
     296          # Also replace any occurrence of this segment in the mappings hash 
     297          mappings.each_pair do |key, value| 
     298            value.gsub!(/:#{name}\b/, ":path[#{count}]") if value.respond_to?(:gsub!) and key != :namespace 
     299          end 
     300        end 
     301        # Force the path regexp to match the whole line 
     302        path = Regexp.new("^#{path}$") 
     303        @@matcher.register_route(Route.new(options.merge(opts).merge(:path => path).merge(mappings), &block)) 
     304      end 
     305      alias :add :simple_route 
     306      alias :simple :simple_route 
     307       
     308      def named_route(name, path, mappings = {}, &block) 
     309        mappings = options.merge(mappings) 
     310        route = simple_route(path, mappings, &block) 
     311        segments = [:protocol, :domain] 
     312      &