Changeset 394

Show
Ignore:
Timestamp:
08/07/07 16:40:18 (1 year ago)
Author:
e.@brainspl.at
Message:

applying update to controller exceptions, closes #115

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/lib/merb/merb_controller.rb

    r382 r394  
    9999    def dispatch(action=:index) 
    100100      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 
    114108      end 
    115        
    116109      @_benchmarks[:action_time] = Time.now - start 
    117110      MERB_LOGGER.info("Time spent in #{self.class}##{action} action: #{@_benchmarks[:action_time]} seconds") 
  • trunk/lib/merb/merb_dispatcher.rb

    r383 r394  
    1515      # the merb RouteMatcher to determine which controller and method to run. 
    1616      # 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] 
    1720      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            } 
    3752          else 
    38             raise Merb::HTTPMethodNotAllowed.new(method, allowed) 
    39           end   
    40         else   
    41           action = route[:action] 
    42         end 
    43         controller._benchmarks[:setup_time] = Time.now - start 
    44         if @@use_mutex 
    45           @@mutex.synchronize { 
    4653            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 
    5061        end 
    5162        [controller, action] 
     
    6677         
    6778        if !File.exist?(path) 
    68           raise "Bad controller! #{cnt}" 
    69         end unless $TESTING 
     79          raise ControllerExceptions::NotFound, "Bad controller! #{cnt}" 
     80        end # unless $TESTING 
    7081         
    7182        begin 
    7283          if MERB_ENV == 'development' 
    73             Object.send(:remove_const, cnt) rescue nil 
     84            Object.send(:remove_const, cnt) 
    7485            load(path) 
    7586          end 
  • trunk/lib/merb/merb_exceptions.rb

    r382 r394  
    55 
    66module 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> 
    848  module ControllerExceptions 
    9     # ControllerExceptions are a way of simplifying controller code by placing 
    10     # exceptional logic elsewhere. ControllerExceptions are like  
    11     # mini-controllers which have one action. The exception action is called  
    12     # to render a web page when the exception is raised. Additionally all 
    13     # ControllerExceptions have an HTTP status code associated with them 
    14     # which is given to the browser when it's action is rendered. 
    15     # 
    16     # ControllerExceptions::Base is an abstract base class, it cannot be 
    17     # raised. Derived from Base are many exceptions, one for each HTTP status 
    18     # code. EG Unauthorized (401), PaymentRequired (402), and Forbidden (403). 
    19     # 
    20     # These exceptions can be raised by your controller without any further 
    21     # work. For example 
    22     # 
    23     #   def show 
    24     #     product = Product.find(params[:id]) 
    25     #     raise NotFound if product.nil? 
    26     #     [...] 
    27     #   end 
    28     # 
    29     # By default the NotFound exception will look in for a template at 
    30     #   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 new 
    34     # exception from one of the already defined ones. Then one must add a 
    35     # special method called error_response which is like a controller action 
    36     # preparing the context for rendering a template at  
    37     #   app/views/exceptions/my_exception_snake_cased.whatevs 
    38     # 
    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::Unauthorized 
    44     #     def error_response 
    45     #       @tried_to_access request.uri 
    46     #       render :layout => 'error_page' 
    47     #     end 
    48     #   end 
    49     # 
    50     # At app/views/exceptions/admin_access_required.rhtml we write 
    51     #  
    52     #   <h1>You're not an administrator!</h1> 
    53     #   <p>You tried to access <%= @tried_to_access %> but that URL is  
    54     #   restricted to administrators.</p> 
    55     # 
    56     # The layout 'error_page' works like any other controller, it would be 
    57     # looked for at app/views/layouts 
    58     # 
    59     # TODO: if you need to pass data to your exception use a constructor. 
    6049    class Base < StandardError 
    6150      include Merb::RenderMixin 
     
    6352      include Merb::ResponderMixin 
    6453       
    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 
    7657        @controller = controller 
    7758      end 
    7859       
    79       def request;  @controller.request;  end       
    80       def params;   @controller.params;   end 
    81       def cookies;  @controller.cookies;  end 
    82       def headers;  @controller.headers;  end 
    83       def session;  @controller.session;  end 
    84       def response; @controller.response; end       
    85        
    86       def status 
    87         self.class.const_get(:STATUS) 
    88       end 
    89        
    9060      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 
    9662      end 
    9763       
    9864      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) 
    10068          render 
    10169        else 
    102           "Error #{status} #{self.class.name.split('::').last}!" 
     70          headers['Content-type'] = 'text/plain' 
     71          "Error #{status} #{self.class.name.split('::').last}! #{message}" 
    10372        end 
    10473      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     
    10692    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 just 
    110       # have these values copied 
    11193      class Continue < Merb::ControllerExceptions::Base; STATUS = 100; end 
    11294      class SwitchingProtocols < Merb::ControllerExceptions::Base; STATUS = 101; end 
  • trunk/specs/merb/merb_dispatch_spec.rb

    r382 r394  
    2424  it "should not allow private and protected methods to be called" do 
    2525    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 
    3127  end   
    3228   
     
    468464  it "should show the error page" do 
    469465    controller, action = request(:get, '/foo/error') 
     466    controller.class.should == AdminAccessRequired 
    470467    controller.body.should == "oh no!" 
    471468  end 
     
    473470  it "should show the error page" do 
    474471    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) 
    476479  end 
    477480end     
  • trunk/specs/merb/merb_render_spec.rb

    r381 r394  
    22require File.dirname(__FILE__) + '/../spec_helper' 
    33 
     4SPEC_VIEW_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', "fixtures/views")) 
    45module Merb 
    56  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 
    711  end 
    812end 
     
    141145  it "should render exception's view without layout" do  
    142146    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))     
    144148    content = x.render 
    145149    content.clean.should == "An Error Occurred!" 
     
    148152  it "should render exception's view with layout" do  
    149153    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)
    151155    content = x.render :layout => 'application' 
    152156    content.clean.should == "An Error Occurred!" 
  • trunk/specs/merb/merb_responder_spec.rb

    r360 r394  
    22 
    33include Merb::ResponderMixin 
     4include Merb::ControllerExceptions::HTTPErrors 
    45 
    56def new_mime(entry,index) 
     
    195196  it "should set status 406 when format is of an unsupported response type" do 
    196197    c = new_controller(:format => 'fromage') 
    197     c.dispatch(:index) 
    198     c.status.should == 406 
     198    lambda{c.dispatch(:index)}.should raise_error(NotAcceptable)     
    199199  end 
    200200   
    201201  it "should set status 406 when no accept header is of a unsupported response type" do 
    202202    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) 
    205204  end 
    206205