Skip to content

Commit

Permalink
lib: implements rescue handler for dispatch_act
Browse files Browse the repository at this point in the history
  • Loading branch information
ryancyq committed Jun 22, 2024
1 parent 1965bd7 commit 740fb79
Show file tree
Hide file tree
Showing 2 changed files with 241 additions and 1 deletion.
69 changes: 68 additions & 1 deletion lib/mime_actor/rescuer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,74 @@ def dispatch_act(action: nil, format: nil, context: self, &block)
block.call
end
rescue Exception => ex
raise
rescue_actor(ex, format:, action:, context:) || raise
end
end

def rescue_actor(error, action: nil, format: nil, context: self, visited: [])
visited << error

if rescuer = dispatch_rescuer(error, format:, action:, context:)
rescuer.call(error, format, action)
error
elsif error && error.cause && !visited.include?(error.cause)
rescue_actor(error.cause, format:, action:, context:, visited:)
end
end

private

def dispatch_rescuer(error, format:, action:, context:)
case rescuer = find_rescuer(error, format:, action:)
when Symbol
rescuer_method = context.method(rescuer)
case rescuer_method.arity
when 0
-> e,f,a { rescuer_method.call }
when 1
-> e,f,a { rescuer_method.call(e) }
when 2
-> e,f,a { rescuer_method.call(e,f) }
else
-> e,f,a { rescuer_method.call(e,f,a) }
end
when Proc
case rescuer.arity
when 0
-> e,f,a { context.instance_exec(&rescuer) }
when 1
-> e,f,a { context.instance_exec(e, &rescuer) }
when 2
-> e,f,a { context.instance_exec(e, f, &rescuer) }
else
-> e,f,a { context.instance_exec(e, f, a, &rescuer) }
end
end
end

def find_rescuer(error, format:, action:)
return unless error

*_, rescuer = actor_rescuers.reverse_each.detect do |rescuee, format_filter, action_filter|
next if action_filter.present? && !Array.wrap(action_filter).include?(action)
next if format_filter.present? && !Array.wrap(format_filter).include?(format)
next unless klazz = constantize_rescuee(rescuee)

klazz === error # klazz is a member of error
end
rescuer
end

def constantize_rescuee(class_or_name)
case class_or_name
when String, Symbol
begin
const_get(class_or_name)
rescue NameError
class_or_name.safe_constantize
end
else
class_or_name
end
end
end
Expand Down
173 changes: 173 additions & 0 deletions spec/mime_actor/rescuer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,177 @@ module AnotherModule; end
end
end
end

describe "when dispatches an action" do
subject(:dispatch) { controller.dispatch(action_name, req, res) }

let(:env) do
{
"REQUEST_METHOD" => "POST",
"HTTP_ACCEPT" => "application/json,application/xml"
}
end
let(:controller) { klazz.new }
let(:req) { ActionDispatch::Request.new(env) }
let(:res) { ActionDispatch::Response.new.tap { |res| res.request = req } }
let(:action_name) { "create" }
let(:format) { "json" }
let(:actor_name) { "#{action_name}_#{format}" }
let(:stub_logger) { instance_double("ActiveSupport::BroadcastLogger") }

before do
klazz.config.logger = stub_logger
end

context "when actor method raise error" do
before do
klazz.class_eval <<-RUBY
def #{action_name}
self.class.dispatch_act(
action: :#{action_name},
format: :#{format},
context: self,
&self.method(:#{actor_name})
).call
end
RUBY
klazz.define_method(actor_name) {}
allow(controller).to receive(actor_name).and_raise(actor_error)
end

context "with catch all rescuer" do
[
RuntimeError.new("My Runtime Error"),
ArgumentError.new("Invalid param")
].each do |error_cause|
context "when raises #{error_cause.class.name}" do
let(:actor_error) { error_cause }
let(:rescuer) { -> ex { logger.debug "rescued #{ex.class.name}" } }

it "handles gracefully" do
klazz.rescue_act_from StandardError, &rescuer

expect(stub_logger).to receive(:debug).with("rescued #{error_cause.class.name}").once
expect { dispatch }.not_to raise_error
end
end
end
end

context "with single format rescuer" do
[
RuntimeError.new("My Runtime Error"),
ArgumentError.new("Invalid param")
].each do |error_cause|
context "when raises #{error_cause.class.name}" do
let(:actor_error) { error_cause }
let(:rescuer) { -> ex, format { logger.debug "rescued #{ex.class.name} with #{format}" } }

it "handles gracefully" do
klazz.rescue_act_from StandardError, format: format.to_sym, &rescuer

expect(stub_logger).to receive(:debug).with("rescued #{error_cause.class.name} with json").once
expect { dispatch }.not_to raise_error
end
end
end
end

context "with multiple formats rescuer" do
[
RuntimeError.new("My Runtime Error"),
ArgumentError.new("Invalid param")
].each do |error_cause|
context "when raises #{error_cause.class.name}" do
let(:actor_error) { error_cause }
let(:rescuer) { -> ex, format { logger.debug "rescued #{ex.class.name} with #{format}" } }

it "handles gracefully" do
klazz.rescue_act_from StandardError, format: [format.to_sym, :pdf], &rescuer

expect(stub_logger).to receive(:debug).with("rescued #{error_cause.class.name} with json").once
expect { dispatch }.not_to raise_error
end
end
end
end

context "with single action rescuer" do
[
RuntimeError.new("My Runtime Error"),
ArgumentError.new("Invalid param")
].each do |error_cause|
context "when raises #{error_cause.class.name}" do
let(:actor_error) { error_cause }
let(:rescuer) { -> ex, _, action { logger.debug "rescued #{ex.class.name} on #{action}" } }

it "handles gracefully" do
klazz.rescue_act_from StandardError, action: action_name.to_sym, &rescuer

expect(stub_logger).to receive(:debug).with("rescued #{error_cause.class.name} on create").once
expect { dispatch }.not_to raise_error
end
end
end
end

context "with multiple actions rescuer" do
[
RuntimeError.new("My Runtime Error"),
ArgumentError.new("Invalid param")
].each do |error_cause|
context "when raises #{error_cause.class.name}" do
let(:actor_error) { error_cause }
let(:rescuer) { -> ex, _, action { logger.debug "rescued #{ex.class.name} on #{action}" } }

it "handles gracefully" do
klazz.rescue_act_from StandardError, action: [action_name.to_sym, :index], &rescuer

expect(stub_logger).to receive(:debug).with("rescued #{error_cause.class.name} on create").once
expect { dispatch }.not_to raise_error
end
end
end
end

context "with multiple rescuers" do
describe "rescues the same error" do
[
RuntimeError.new("My Runtime Error"),
ArgumentError.new("Invalid param")
].each do |error_cause|
context "when raises #{error_cause.class.name}" do
let(:actor_error) { error_cause }
let(:rescuer) { -> ex { logger.debug "rescued #{ex.class.name}" } }
let(:another_rescuer) { -> ex { logger.debug "rescued another #{ex.class.name}" } }

it "resolve using most recently declared rescuer" do
klazz.rescue_act_from StandardError, &rescuer
klazz.rescue_act_from StandardError, &another_rescuer

expect(stub_logger).to receive(:debug).with("rescued another #{error_cause.class.name}").once
expect { dispatch }.not_to raise_error
end
end
end
end

describe "rescues the different error" do
context "when raises ArgumentError" do
let(:actor_error) { ArgumentError.new("Invalid param") }
let(:rescuer) { -> ex { logger.debug "rescued #{ex.class.name}" } }
let(:another_rescuer) { -> ex { logger.debug "rescued different #{ex.class.name}" } }

it "resolve using the correct rescuer" do
klazz.rescue_act_from ArgumentError, &rescuer
klazz.rescue_act_from RuntimeError, &another_rescuer

expect(stub_logger).to receive(:debug).with("rescued ArgumentError").once
expect { dispatch }.not_to raise_error
end
end
end
end
end
end
end

0 comments on commit 740fb79

Please sign in to comment.