Changeset 394
- Timestamp:
- 08/07/07 16:40:18 (1 year ago)
- Files:
-
- trunk/lib/merb/merb_controller.rb (modified) (1 diff)
- trunk/lib/merb/merb_dispatcher.rb (modified) (2 diffs)
- trunk/lib/merb/merb_exceptions.rb (modified) (2 diffs)
- trunk/specs/merb/merb_dispatch_spec.rb (modified) (3 diffs)
- trunk/specs/merb/merb_render_spec.rb (modified) (3 diffs)
- trunk/specs/merb/merb_responder_spec.rb (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/lib/merb/merb_controller.rb
r382 r394 99 99 def dispatch(action=:index) 100 100 start = Time.now 101 begin 102 if !self.class.callable_actions.include?(action.to_s) 103 raise NotFound 104 MERB_LOGGER.info "Action: #{action} not in callable_actions: #{self.class.callable_actions}" 105 else 106 setup_session 107 super(action) 108 finalize_session 109 end 110 rescue ControllerExceptions::Base => e 111 e.set_controller(self) # for access to session, params, etc 112 @_body = e.call_action 113 set_status(e.status) 101 if !self.class.callable_actions.include?(action.to_s) 102 raise NotFound 103 MERB_LOGGER.info "Action: #{action} not in callable_actions: #{self.class.callable_actions}" 104 else 105 setup_session 106 super(action) 107 finalize_session 114 108 end 115 116 109 @_benchmarks[:action_time] = Time.now - start 117 110 MERB_LOGGER.info("Time spent in #{self.class}##{action} action: #{@_benchmarks[:action_time]} seconds") trunk/lib/merb/merb_dispatcher.rb
r383 r394 15 15 # the merb RouteMatcher to determine which controller and method to run. 16 16 # Returns a 2 element tuple of: [controller, action] 17 # 18 # ControllerExceptions are rescued here and returned as the controller. 19 # In that case the function will return [exception, :error_response] 17 20 def handle(request, response) 18 start = Time.now 19 20 request_uri = request.params[Merb::Const::REQUEST_URI] 21 request_uri.sub!(path_prefix, '') if path_prefix 22 route = route_path(request_uri) 23 24 allowed = route.delete(:allowed) 25 rest = route.delete(:rest) 26 namespace = route.delete(:namespace) 27 28 cont = namespace ? "#{namespace}/#{route[:controller]}" : route[:controller] 29 30 klass = resolve_controller(cont) 31 controller = klass.build(request.body, request.params, route, response) 32 33 if rest 34 method = controller.request.method 35 if allowed.keys.include?(method) && action = allowed[method] 36 controller.params[:action] = action 21 begin 22 start = Time.now 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 47 controller._benchmarks[:setup_time] = Time.now - start 48 if @@use_mutex 49 @@mutex.synchronize { 50 controller.dispatch(action) 51 } 37 52 else 38 raise Merb::HTTPMethodNotAllowed.new(method, allowed)39 end40 else41 action = route[:action]42 end43 controller._benchmarks[:setup_time] = Time.now - start44 if @@use_mutex45 @@mutex.synchronize {46 53 controller.dispatch(action) 47 } 48 else 49 controller.dispatch(action) 54 end 55 rescue ControllerExceptions::Base => exception 56 exception.set_env(request, response, controller) # for access to session, params, etc 57 exception.call_action 58 59 controller = exception 60 action = :error_response 50 61 end 51 62 [controller, action] … … 66 77 67 78 if !File.exist?(path) 68 raise "Bad controller! #{cnt}"69 end unless $TESTING79 raise ControllerExceptions::NotFound, "Bad controller! #{cnt}" 80 end # unless $TESTING 70 81 71 82 begin 72 83 if MERB_ENV == 'development' 73 Object.send(:remove_const, cnt) rescue nil84 Object.send(:remove_const, cnt) 74 85 load(path) 75 86 end trunk/lib/merb/merb_exceptions.rb
r382 r394 5 5 6 6 module Merb 7 7 # ControllerExceptions are a way of simplifying controller code by placing 8 # exceptional logic elsewhere. ControllerExceptions are mini-controllers 9 # which have only one action: error_response 10 # 11 # Additionally all ControllerExceptions have an HTTP status code associated 12 # with them which is sent to the browser when it is rendered. 13 # 14 # ControllerExceptions::Base is the abstract base class of all 15 # ControllerExceptions. Derived from Base are exceptions for each HTTP 16 # status code. These are addressed ControllerExceptions::HTTPErrors. 17 # 18 # These exceptions can be raised by your controller. For example 19 # 20 # def show 21 # product = Product.find(params[:id]) 22 # raise NotFound if product.nil? 23 # [...] 24 # end 25 # 26 # The NotFound exception will look for a template at 27 # app/views/exceptions/not_found.erb 28 # If the template is not found then a simple message will be sent. 29 # 30 # To extend the use of the ControllerExceptions one may extend any of the 31 # HTTPError classes. One must then add the special 'action' method called 32 # error_response. 33 # 34 # As an example we can create an exception called AdminAccessRequired. 35 # 36 # class AdminAccessRequired < Merb::ControllerExceptions::Unauthorized 37 # def error_response 38 # @tried_to_access request.uri 39 # render :layout => 'error_page' 40 # end 41 # end 42 # 43 # In app/views/exceptions/admin_access_required.rhtml 44 # 45 # <h1>You're not an administrator!</h1> 46 # <p>You tried to access <%= @tried_to_access %> but that URL is 47 # restricted to administrators.</p> 8 48 module ControllerExceptions 9 # ControllerExceptions are a way of simplifying controller code by placing10 # exceptional logic elsewhere. ControllerExceptions are like11 # mini-controllers which have one action. The exception action is called12 # to render a web page when the exception is raised. Additionally all13 # ControllerExceptions have an HTTP status code associated with them14 # which is given to the browser when it's action is rendered.15 #16 # ControllerExceptions::Base is an abstract base class, it cannot be17 # raised. Derived from Base are many exceptions, one for each HTTP status18 # code. EG Unauthorized (401), PaymentRequired (402), and Forbidden (403).19 #20 # These exceptions can be raised by your controller without any further21 # work. For example22 #23 # def show24 # product = Product.find(params[:id])25 # raise NotFound if product.nil?26 # [...]27 # end28 #29 # By default the NotFound exception will look in for a template at30 # app/views/exceptions/not_found.{rhtml, haml, whatevs}31 # If the template is not found then a simple message will be sent.32 #33 # To extend the use of the ControllerExceptions one may derive a new34 # exception from one of the already defined ones. Then one must add a35 # special method called error_response which is like a controller action36 # preparing the context for rendering a template at37 # app/views/exceptions/my_exception_snake_cased.whatevs38 #39 # As an example we will create an exception called AdminAccessRequired.40 # First we decide on what HTTP status code such an error might have.41 # 401, Unauthorized seems to fit.42 #43 # class AdminAccessRequired < Merb::ControllerExceptions::Unauthorized44 # def error_response45 # @tried_to_access request.uri46 # render :layout => 'error_page'47 # end48 # end49 #50 # At app/views/exceptions/admin_access_required.rhtml we write51 #52 # <h1>You're not an administrator!</h1>53 # <p>You tried to access <%= @tried_to_access %> but that URL is54 # restricted to administrators.</p>55 #56 # The layout 'error_page' works like any other controller, it would be57 # looked for at app/views/layouts58 #59 # TODO: if you need to pass data to your exception use a constructor.60 49 class Base < StandardError 61 50 include Merb::RenderMixin … … 63 52 include Merb::ResponderMixin 64 53 65 def _template_root 66 @controller._template_root 67 end 68 69 # The filename of the exception template. usually something like 70 # views/exceptions/not_found.rhtml 71 def template_path 72 _template_root / 'exceptions' / self.class.name.snake_case.split('::').last 73 end 74 75 def set_controller(controller) 54 def set_env(request, response, controller = nil) 55 @request = request 56 @response = response 76 57 @controller = controller 77 58 end 78 59 79 def request; @controller.request; end80 def params; @controller.params; end81 def cookies; @controller.cookies; end82 def headers; @controller.headers; end83 def session; @controller.session; end84 def response; @controller.response; end85 86 def status87 self.class.const_get(:STATUS)88 end89 90 60 def call_action 91 # TODO: this method is meaningless at the moment but it could call 92 # filters 93 # before filters? 94 error_response 95 # after filters? 61 @_body = error_response 96 62 end 97 63 98 64 def error_response 99 if File.exists?(template_path) 65 # TODO: the find_template call below is pretty messy. calling render 66 # and rescuing an exception if the file doesn't exist would be better 67 if find_template(:action => self.class.name.snake_case.split('::').last) 100 68 render 101 69 else 102 "Error #{status} #{self.class.name.split('::').last}!" 70 headers['Content-type'] = 'text/plain' 71 "Error #{status} #{self.class.name.split('::').last}! #{message}" 103 72 end 104 73 end 105 end 74 75 # Below are methods which make the exception act like a controller 76 77 def headers 78 if @controller 79 @controller.headers 80 else 81 @_headers ||= {'Content-Type' => 'text/html'} 82 end 83 end 84 attr_reader :request, :response 85 def params; @controller.params if @controller; end 86 def cookies; @controller.cookies if @controller; end 87 def session; @controller.session if @controller; end 88 def status; self.class::STATUS; end 89 def body; @_body; end 90 end 91 106 92 module HTTPErrors 107 # created with:108 # ruby -r rubygems -e "require 'mongrel'; puts Mongrel::HTTP_STATUS_CODES.keys.sort.map {|code| %Q{class #{Mongrel::HTTP_STATUS_CODES[code].gsub /[^\w]/,''} < Merb::ControllerExceptions::Base; STATUS = #{code}; end} }.join(%Q{\n})"109 # we could do some magic here, but i think it's more instructive to just110 # have these values copied111 93 class Continue < Merb::ControllerExceptions::Base; STATUS = 100; end 112 94 class SwitchingProtocols < Merb::ControllerExceptions::Base; STATUS = 101; end trunk/specs/merb/merb_dispatch_spec.rb
r382 r394 24 24 it "should not allow private and protected methods to be called" do 25 25 controller, action = request(:get, '/foo/call_filters') 26 controller.class.should == Foo 27 x = Merb::ControllerExceptions::NotFound.new 28 x.set_controller(controller) 29 controller.body.should == x.error_response 30 controller.status.should == x.status 26 controller.class.should == NotFound 31 27 end 32 28 … … 468 464 it "should show the error page" do 469 465 controller, action = request(:get, '/foo/error') 466 controller.class.should == AdminAccessRequired 470 467 controller.body.should == "oh no!" 471 468 end … … 473 470 it "should show the error page" do 474 471 controller, action = request(:get, '/foo/raise404') 475 controller.body.should == "Error 404 NotFound!" 472 controller.class.should == NotFound 473 end 474 475 it "should show the NotFound if we call unknown path" do 476 lambda do 477 Merb::Dispatcher.resolve_controller('not_a_real_controller') 478 end.should raise_error(NotFound) 476 479 end 477 480 end trunk/specs/merb/merb_render_spec.rb
r381 r394 2 2 require File.dirname(__FILE__) + '/../spec_helper' 3 3 4 SPEC_VIEW_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', "fixtures/views")) 4 5 module Merb 5 6 class Controller 6 self._template_root = File.expand_path(File.join(File.dirname(__FILE__), '..', "fixtures/views")) 7 self._template_root = SPEC_VIEW_ROOT 8 end 9 class ControllerExceptions::Base 10 def self._template_root; SPEC_VIEW_ROOT; end 7 11 end 8 12 end … … 141 145 it "should render exception's view without layout" do 142 146 x = AdminAccessRequired.new 143 x.set_ controller Examples.build(@in.req, @in.env, {}, @res)147 x.set_env(@in.req, @res, Examples.build(@in.req, @in.env, {}, @res)) 144 148 content = x.render 145 149 content.clean.should == "An Error Occurred!" … … 148 152 it "should render exception's view with layout" do 149 153 x = AdminAccessRequired.new 150 x.set_ controller Examples.build(@in.req, @in.env, {}, @res)154 x.set_env(@in.req, @res, Examples.build(@in.req, @in.env, {}, @res)) 151 155 content = x.render :layout => 'application' 152 156 content.clean.should == "An Error Occurred!" trunk/specs/merb/merb_responder_spec.rb
r360 r394 2 2 3 3 include Merb::ResponderMixin 4 include Merb::ControllerExceptions::HTTPErrors 4 5 5 6 def new_mime(entry,index) … … 195 196 it "should set status 406 when format is of an unsupported response type" do 196 197 c = new_controller(:format => 'fromage') 197 c.dispatch(:index) 198 c.status.should == 406 198 lambda{c.dispatch(:index)}.should raise_error(NotAcceptable) 199 199 end 200 200 201 201 it "should set status 406 when no accept header is of a unsupported response type" do 202 202 c = new_controller(:http_accept => 'stale/crackers;q=0.7,camel/milk;q=1.0') 203 c.dispatch(:index) 204 c.status.should == 406 203 lambda{c.dispatch(:index)}.should raise_error(NotAcceptable) 205 204 end 206 205
