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

Added ensure #206

Open
wants to merge 10 commits into
base: master
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
6 changes: 1 addition & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
name: Run Tests

on:
push:
branches:
- "master"
- "v3"
on: [push, pull_request]

jobs:
spec:
Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,27 @@ around after 2
around after 1
```

#### Ensure Hooks

Sometimes an interactor needs to perform an action even if the context fails like the
typical begin ensure block.

```ruby
ensure_do do
context.finish_time = Time.now
end
```

A symbol argument can also be given, rather than a block.

```ruby
ensure_do :set_finish_time

def set_finish_time
context.finish_time = Time.now
end
```

#### Interactor Concerns

An interactor can define multiple before/after hooks, allowing common hooks to
Expand Down Expand Up @@ -405,7 +426,9 @@ purpose is to run *other* interactors.
class PlaceOrder
include Interactor::Organizer

organize CreateOrder, ChargeCard, SendThankYou
organize CreateOrder, ChargeCard

ensure_do SendThankYou
end
```

Expand Down
69 changes: 67 additions & 2 deletions lib/interactor/hooks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,43 @@ def after(*hooks, &block)
hooks.each { |hook| after_hooks.unshift(hook) }
end

# Public: Declare hooks to run after hooks in an ensure block.
# The ensure method may be called multiple times; subsequent calls prepend declared
# hooks to existing ensure hooks.
#
# hooks - Zero or more Symbol method names representing instance methods
# to be called after the hooks invocations.
# block - An optional block to be executed as a hook. If given, the block
# is executed before methods corresponding to any given Symbols.
#
# Examples
#
# class MyInteractor
# include Interactor
#
# ensure_do :set_finish_time
#
# ensure_do do
# puts "finished"
# end
#
# def call
# puts "called"
# end
#
# private
#
# def set_finish_time
# context.finish_time = Time.now
# end
# end
#
# Returns nothing.
def ensure_do(*hooks, &block)
hooks << block if block
hooks.each { |hook| ensure_hooks.unshift(hook) }
end

# Internal: An Array of declared hooks to run around Interactor
# invocation. The hooks appear in the order in which they will be run.
#
Expand Down Expand Up @@ -165,7 +202,7 @@ def before_hooks
@before_hooks ||= []
end

# Internal: An Array of declared hooks to run before Interactor
# Internal: An Array of declared hooks to run after Interactor
# invocation. The hooks appear in the order in which they will be run.
#
# Examples
Expand All @@ -183,6 +220,25 @@ def before_hooks
def after_hooks
@after_hooks ||= []
end

# Internal: An Array of declared hooks to run after the hooks
# invocation. The hooks appear in the order in which they will be run.
#
# Examples
#
# class MyInteractor
# include Interactor
#
# ensure_do :set_finish_time, :say_goodbye
# end
#
# MyInteractor.ensure_hooks
# # => [:say_goodbye, :set_finish_time]
#
# Returns an Array of Symbols and Procs.
def ensure_hooks
@ensure_hooks ||= []
end
end

private
Expand Down Expand Up @@ -213,6 +269,8 @@ def with_hooks
yield
run_after_hooks
end
ensure
run_ensure_hooks
end

# Internal: Run around hooks.
Expand All @@ -238,7 +296,14 @@ def run_after_hooks
run_hooks(self.class.after_hooks)
end

# Internal: Run a colection of hooks. The "run_hooks" method is the common
# Internal: Run ensure hooks.
#
# Returns nothing.
def run_ensure_hooks
run_hooks(self.class.ensure_hooks)
end

# Internal: Run a collection of hooks. The "run_hooks" method is the common
# interface by which collections of either before or after hooks are run.
#
# hooks - An Array of Symbol and Proc hooks.
Expand Down
55 changes: 55 additions & 0 deletions lib/interactor/organizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,33 @@ def organize(*interactors)
@organized = interactors.flatten
end

# Public: Declare Interactors to be invoked as part of the
# Interactor::Organizer's invocation in an ensured block. These
# interactors are invoked in the order in which they are declared.
#
# interactors - Zero or more (or an Array of) Interactor classes.
#
# Examples
#
# class MyFirstOrganizer
# include Interactor::Organizer
#
# organize InteractorOne, InteractorTwo
# ensure_do InteractorThree, InteractorFour
# end
#
# class MySecondOrganizer
# include Interactor::Organizer
#
# organize [InteractorThree, InteractorFour]
# ensure_do [InteractorFive, InteractorSix]
# end
#
# Returns nothing.
def ensure_do(*interactors)
@ensured = interactors.flatten
end

# Internal: An Array of declared Interactors to be invoked.
#
# Examples
Expand All @@ -65,6 +92,25 @@ def organize(*interactors)
def organized
@organized ||= []
end

# Internal: An Array of declared Interactors to be invoked in an ensure block.
#
# Examples
#
# class MyOrganizer
# include Interactor::Organizer
#
# organize InteractorOne, InteractorTwo
# ensure_do InteractorThree, InteractorFour
# end
#
# MyOrganizer.ensured
# # => [InteractorThree, InteractorFour]
#
# Returns an Array of Interactor classes or an empty Array.
def ensured
@ensured ||= []
end
end

# Internal: Interactor::Organizer instance methods.
Expand All @@ -75,9 +121,18 @@ module InstanceMethods
#
# Returns nothing.
def call
call!
end

def call!
self.class.organized.each do |interactor|
context._current_interactor_class = interactor
interactor.call!(context)
end
ensure
self.class.ensured.each do |interactor|
interactor.call(context)
end
end
end
end
Expand Down
128 changes: 126 additions & 2 deletions spec/interactor/hooks_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ def build_hooked(&block)
hooked.class_eval do
attr_reader :steps

def self.create
new
end

def self.process
new.tap(&:process).steps
end
Expand Down Expand Up @@ -306,7 +310,95 @@ def add_after2
end
end

context "with around, before and after hooks" do
context "with an ensure hook method" do
let(:hooked) {
build_hooked do
ensure_do :add_ensure

private

def add_ensure
steps << :ensure
end
end
}

it "runs the after hook methods" do
expect(hooked.process).to eq([
:process,
:ensure
])
end
end

context "with an ensure hook block" do
let(:hooked) {
build_hooked do
ensure_do do
steps << :ensure
end
end
}

it "runs the after hook blocks" do
expect(hooked.process).to eq([
:process,
:ensure
])
end
end

context "with an ensure hook method and block in one call" do
let(:hooked) {
build_hooked do
ensure_do :add_ensure1 do
steps << :ensure2
end

private

def add_ensure1
steps << :ensure1
end
end
}

it "runs the after hook method and block in order" do
expect(hooked.process).to eq([
:process,
:ensure2,
:ensure1
])
end
end

context "with an ensure hook method and block in multiple calls" do
let(:hooked) {
build_hooked do
after do
steps << :ensure1
end

after :add_ensure2

private

def add_ensure2
steps << :ensure2
end
end
}

it "runs the after hook block and method in order" do
expect(hooked.process).to eq([
:process,
:ensure2,
:ensure1
])
end
end

context "with around, before, after and ensure hooks" do
let(:hooked) {
build_hooked do
around do |hooked|
Expand Down Expand Up @@ -336,6 +428,14 @@ def add_after2
after do
steps << :after2
end

ensure_do do
steps << :ensure1
end

ensure_do do
steps << :ensure2
end
end
}

Expand All @@ -349,10 +449,34 @@ def add_after2
:after2,
:after1,
:around_after2,
:around_after1
:around_after1,
:ensure2,
:ensure1
])
end
end

context "ensure hook run even if an exception is raised" do
let(:hooked) {
build_hooked do
ensure_do :add_ensure

private

def add_ensure
steps << :ensure
end
end
}

it "run the ensure hook" do
allow_any_instance_of(Interactor::Hooks).to receive(:run_around_hooks).and_raise(Failure)
object = hooked.create

expect { object.process }.to raise_error(Failure)
expect(object.steps).to eq([:ensure])
end
end
end
end
end
Loading