Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace raise with throw to handle context failure #133

Open
wants to merge 2 commits into
base: v4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions lib/interactor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,16 @@ def initialize(context = {})
#
# Returns nothing.
def run
run!
rescue Failure
catch(:early_return) do
with_hooks do
call
context.called!(self)
end
end
context.rollback! if context.failure?
rescue
context.rollback!
raise
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see if we could preserve how the primary functionality lived in run! rather than in run.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I need to stare at and think about this for a little while. I'm not as familiar with throw and catch as I am with raise and rescue, so we just need to think through the edge cases.

end

# Internal: Invoke an Interactor instance along with all defined hooks. The
Expand All @@ -139,13 +147,8 @@ def run
# Returns nothing.
# Raises Interactor::Failure if the context is failed.
def run!
with_hooks do
call
context.called!(self)
end
rescue
context.rollback!
raise
run
raise(Failure, context) if context.failure?
end

# Public: Invoke an Interactor instance without any hooks, tracking, or
Expand Down
6 changes: 5 additions & 1 deletion lib/interactor/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ def failure?
def fail!(context = {})
context.each { |key, value| modifiable[key.to_sym] = value }
@failure = true
raise Failure, self
signal_early_return!
end

def signal_early_return!
throw :early_return
end

# Internal: Track that an Interactor has been called. The "called!" method
Expand Down
5 changes: 3 additions & 2 deletions lib/interactor/organizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module Interactor
# class MyOrganizer
# include Interactor::Organizer
#
# organizer InteractorOne, InteractorTwo
# organize InteractorOne, InteractorTwo
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

# end
module Organizer
# Internal: Install Interactor::Organizer's behavior in the given class.
Expand Down Expand Up @@ -77,7 +77,8 @@ module InstanceMethods
# Returns nothing.
def call
self.class.organized.each do |interactor|
interactor.call!(context)
context.signal_early_return! if context.failure?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've extracted throwing symbol from Context#fail to a dedicated method so we can avoid logic duplication.

interactor.call(context)
end
end
end
Expand Down
12 changes: 2 additions & 10 deletions spec/interactor/context_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,10 @@ module Interactor
}.from("bar").to("baz")
end

it "raises failure" do
it "throws :early_return" do
expect {
context.fail!
}.to raise_error(Failure)
end

it "makes the context available from the failure" do
begin
context.fail!
rescue Failure => error
expect(error.context).to eq(context)
end
}.to throw_symbol(:early_return)
end
end

Expand Down
24 changes: 17 additions & 7 deletions spec/interactor/organizer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,35 @@ module Interactor

describe "#call" do
let(:instance) { organizer.new }
let(:context) { double(:context) }
let(:context) { double(:context, failure?: false) }
let(:interactor2) { double(:interactor2) }
let(:interactor3) { double(:interactor3) }
let(:interactor4) { double(:interactor4) }
let(:organized_interactors) { [interactor2, interactor3, interactor4] }

before do
allow(instance).to receive(:context) { context }
allow(organizer).to receive(:organized) {
[interactor2, interactor3, interactor4]
}
allow(organizer).to receive(:organized) { organized_interactors }
organized_interactors.each do |organized_interactor|
allow(organized_interactor).to receive(:call)
end
end

it "calls each interactor in order with the context" do
expect(interactor2).to receive(:call!).once.with(context).ordered
expect(interactor3).to receive(:call!).once.with(context).ordered
expect(interactor4).to receive(:call!).once.with(context).ordered
expect(interactor2).to receive(:call).once.with(context).ordered
expect(interactor3).to receive(:call).once.with(context).ordered
expect(interactor4).to receive(:call).once.with(context).ordered

instance.call
end

it "signals about early_return on failure of one of organizers" do
allow(context).to receive(:failure?).and_return(false, true)
expect(context).to receive(:signal_early_return!).and_throw(:foo)
expect {
instance.call
}.to throw_symbol
end
end
end
end
80 changes: 60 additions & 20 deletions spec/support/lint.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
shared_examples :lint do
let(:interactor) { Class.new.send(:include, described_class) }

let(:context_double) do
double(:double, failure?: false, called!: nil, rollback!: nil)
end

let(:failed_context_double) do
double(:failed_context_double, failure?: true, called!: nil, rollback!: nil)
end

describe ".call" do
let(:context) { double(:context) }
let(:instance) { double(:instance, context: context) }
Expand Down Expand Up @@ -66,52 +74,84 @@
let(:instance) { interactor.new }

it "runs the interactor" do
expect(instance).to receive(:run!).once.with(no_args)
expect(instance).to receive(:call).once.with(no_args)

instance.run
end

it "rescues failure" do
expect(instance).to receive(:run!).and_raise(Interactor::Failure)

it "catches :early_return" do
allow(instance).to receive(:call).and_throw(:early_return)
expect {
instance.run
}.not_to raise_error
}.not_to throw_symbol
end

it "raises other errors" do
expect(instance).to receive(:run!).and_raise("foo")
context "when error is raised inside #call" do
it "propagates it and rollbacks context" do
allow(instance).to receive(:context) { context_double }
allow(instance).to receive(:call).and_raise("foo")

expect {
expect(instance.context).to receive(:rollback!)
expect {
instance.run
}.to raise_error("foo")
end
end

context "on call failure" do
before do
allow(instance).to receive(:context) { failed_context_double }
end

it "doesn't raise Failure" do
expect {
instance.run
}.not_to raise_error
end

it "rollbacks context on error" do
expect(instance.context).to receive(:rollback!)
instance.run
}.to raise_error("foo")
end
end
end

describe "#run!" do
let(:instance) { interactor.new }

it "calls the interactor" do
expect(instance).to receive(:call).once.with(no_args)
expect(instance).to receive(:run).once.with(no_args)

instance.run!
end

it "raises failure" do
expect(instance).to receive(:run!).and_raise(Interactor::Failure)

expect {
instance.run!
}.to raise_error(Interactor::Failure)
end

it "raises other errors" do
expect(instance).to receive(:run!).and_raise("foo")
it "propagates errors" do
expect(instance).to receive(:run).and_raise("foo")

expect {
instance.run
}.to raise_error("foo")
end

context "on failure" do
before do
allow(instance).to receive(:context) { failed_context_double }
end

it "raises Interactor::Failure" do
expect {
instance.run!
}.to raise_error(Interactor::Failure)
end

it "makes context available from the error" do
begin
instance.run!
rescue Interactor::Failure => error
expect(error.context).to be(instance.context)
end
end
end
end

describe "#call" do
Expand Down