Changeset 429
- Timestamp:
- 08/26/07 22:29:41 (1 year ago)
- Files:
-
- trunk/History.txt (modified) (1 diff)
- trunk/Rakefile (modified) (1 diff)
- trunk/lib/merb/controller.rb (modified) (2 diffs)
- trunk/lib/merb/core_ext.rb (modified) (1 diff)
- trunk/lib/merb/core_ext/string.rb (modified) (1 diff)
- trunk/lib/merb/dispatcher.rb (modified) (3 diffs)
- trunk/lib/merb/mixins/controller.rb (modified) (1 diff)
- trunk/lib/merb/request.rb (modified) (1 diff)
- trunk/lib/merb/router.rb (modified) (7 diffs)
- trunk/specs/merb/merb_controller_filters_spec.rb (modified) (5 diffs)
- trunk/specs/merb/merb_controller_spec.rb (modified) (1 diff)
- trunk/specs/merb/merb_dispatch_spec.rb (modified) (5 diffs)
- trunk/specs/merb/merb_mail_controller_spec.rb (modified) (2 diffs)
- trunk/specs/merb/merb_render_spec.rb (modified) (15 diffs)
- trunk/specs/merb/merb_responder_spec.rb (modified) (15 diffs)
- trunk/specs/merb/merb_router_spec.rb (modified) (3 diffs)
- trunk/specs/merb/merb_view_context_spec.rb (modified) (6 diffs)
- trunk/specs/spec_helper.rb (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/History.txt
r419 r429 5 5 * Fixed bug where image_tag would prepend /images/ to urls starting with http:// or https://. 6 6 * 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. 7 9 8 10 See also: trunk/Rakefile
r427 r429 123 123 end 124 124 125 desc "Run a specific spec with TASK=xxxx" 126 Spec::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"] 130 end 131 125 132 desc "Run all specs output html" 126 133 Spec::Rake::SpecTask.new('specs_html') do |t| trunk/lib/merb/controller.rb
r401 r429 32 32 end 33 33 34 def build( req, env, args, resp)34 def build(status, headers, response, request, params, cookies) 35 35 cont = new 36 cont. parse_request(req, env, args, resp)36 cont.set_dispatch_variables(request, status, headers, response, params, cookies) 37 37 cont 38 38 end 39 39 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] 70 45 # This condition allows for certain controller/action paths to allow a 71 46 # session ID to be passed in a query string. This is needed for Flash … … 73 48 # running session.regenerate after any controller taking advantage of 74 49 # 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 77 52 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 90 55 end 91 92 93 56 94 57 def dispatch(action=:index) 95 58 start = Time.now trunk/lib/merb/core_ext.rb
r401 r429 1 corelib = __DIR__+'/merb/core_ext'1 corelib = File.dirname(__FILE__)+'/core_ext' 2 2 3 3 %w[ inflector trunk/lib/merb/core_ext/string.rb
r400 r429 16 16 end 17 17 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 18 34 end trunk/lib/merb/dispatcher.rb
r412 r429 22 22 start = Time.now 23 23 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 47 37 controller._benchmarks[:setup_time] = Time.now - start 48 38 if @@use_mutex 49 @@mutex.synchronize { 50 controller.dispatch(action) 51 } 39 @@mutex.synchronize { controller.dispatch(action) } 52 40 else 53 41 controller.dispatch(action) … … 63 51 end 64 52 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] 69 91 end 70 92 … … 87 109 end 88 110 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 89 221 end # end class << self 90 222 trunk/lib/merb/mixins/controller.rb
r425 r429 45 45 46 46 protected 47 NAME_REGEX = /Content-Disposition:.* name="?([^\";]*)"?/ni.freeze48 CONTENT_TYPE_REGEX = /Content-Type: (.*)\r\n/ni.freeze49 FILENAME_REGEX = /Content-Disposition:.* filename="?([^\";]*)"?/ni.freeze50 CRLF = "\r\n".freeze51 EOL = CRLF52 def parse_multipart(request,boundary,env)53 boundary = "--#{boundary}"54 paramhsh = {}55 buf = ""56 content_length = env['CONTENT_LENGTH'].to_i57 input = request58 input.binmode if defined? input.binmode59 boundary_size = boundary.size + EOL.size60 bufsize = 1638461 content_length -= boundary_size62 status = input.read(boundary_size)63 raise EOFError, "bad content body" unless status == boundary + EOL64 rx = /(?:#{EOL})?#{Regexp.quote(boundary,'n')}(#{EOL}|--)/65 loop {66 head = nil67 body = ''68 filename = content_type = name = nil69 read_size = 070 until head && buf =~ rx71 i = buf.index("\r\n\r\n")72 if( i == nil && read_size == 0 && content_length == 0 )73 content_length = -174 break75 end76 if !head && i77 head = buf.slice!(0, i+2) # First \r\n78 buf.slice!(0, 2) # Second \r\n79 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.binmode86 end87 next88 end89 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 end95 96 read_size = bufsize < content_length ? bufsize : content_length97 if( read_size > 0 )98 c = input.read(read_size)99 raise EOFError, "bad content body" if c.nil? || c.empty?100 buf << c101 content_length -= c.size102 end103 end104 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 end112 113 if filename && !filename.empty?114 body.rewind115 data = {116 :filename => File.basename(filename),117 :content_type => content_type,118 :tempfile => body,119 :size => File.size(body)120 }121 else122 data = body123 end124 paramhsh = normalize_params(paramhsh,name,data)125 break if buf.empty? || content_length == -1126 }127 paramhsh128 end129 130 def normalize_params(parms, key, val)131 key =~ /\]?\[/132 before = $`133 after = $'134 135 if before.nil?136 parms[key.sub("]", "")] = val137 elsif after == "]"138 (parms[before] ||= []) << val139 else140 parms[before] ||= {}141 parms[before] = normalize_params(parms[before], after, val)142 end143 parms144 end145 146 # parses a query string or the payload of a POST147 # request into the params hash. So for example:148 # /foo?bar=nik&post[title]=heya&post[body]=whatever149 # 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 end156 157 47 # render using chunked encoding 158 48 # def stream trunk/lib/merb/request.rb
r399 r429 119 119 end 120 120 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 '/' 122 123 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 124 127 end 125 128 trunk/lib/merb/router.rb
r399 r429 2 2 3 3 class Router 4 SEGMENT_REGEXP = /(:[a-z*_]+)/.freeze 5 LOOKAHEAD_SEGMENT_REGEXP = /(?::([a-z*_]+))/.freeze 6 PARENTHETICAL_SEGMENT_STRING = "([^\/.,;?]+)".freeze 4 7 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 7 52 class << self 53 include Merb::Router::Options 8 54 9 55 def prepare … … 15 61 @@matcher.compile_router 16 62 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) 20 67 end 21 68 … … 28 75 end 29 76 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) 32 83 end 33 84 … … 36 87 end 37 88 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 &:call44 else45 generate_resources_routes(res,opts)46 end47 end48 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 &:call55 else56 generate_singleton_routes(res,opts)57 end58 end59 60 89 def default_routes(*a) 61 90 @@matcher.default_routes(*a) … … 65 94 @@matcher.compiled_statement 66 95 end 67 68 def compiled_regexen69 @@matcher.compiled_regexen70 end71 72 def generate_resources_routes(res, opts)73 [@@matcher,@@generator].each { |r| r.generate_resources_routes(res, opts) }74 end75 76 def generate_singleton_routes(res, opts)77 [@@matcher,@@generator].each { |r| r.generate_singleton_routes(res, opts) }78 end79 80 96 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 81 177 82 178 class RouteMatcher … … 90 186 end 91 187 188 def register_route(route) 189 @routes << route 190 route 191 end 192 92 193 # 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) 105 196 end 106 197 … … 109 200 # that matches wins. 110 201 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 275 end 276 277 # Modify Router class by adding the simple_route method 278 module 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" 127 290 name = $1 128 (name =~ /(\*+)(\w+)/) ? (flag = true; name = $2) : (flag = false)129 291 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 &
