Skip to content

Commit

Permalink
Support conditional execution of organized interactors (Fixes collect…
Browse files Browse the repository at this point in the history
  • Loading branch information
hedgesky committed Apr 25, 2017
1 parent 15e70dd commit 1dab0bf
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 19 deletions.
1 change: 1 addition & 0 deletions lib/interactor.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require "interactor/context"
require "interactor/error"
require "interactor/hooks"
require "interactor/organized_interactor"
require "interactor/organizer"

# Public: Interactor methods. Because Interactor is a module, custom Interactor
Expand Down
40 changes: 40 additions & 0 deletions lib/interactor/organized_interactor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module Interactor
# Internal: this class allows us to run interactors with regard of given
# options, such as :if, :unless, :before, :after
class OrganizedInteractor
attr_reader :interactor, :options

def initialize(interactor, options = {})
@interactor = interactor
@options = options
end

def call!(context, within_organizer)
interactor.call!(context) if permitted_to_call?(within_organizer)
end

private

def permitted_to_call?(organizer)
permitted_by_if?(organizer) && permitted_by_unless?(organizer)
end

def permitted_by_if?(organizer)
return true unless options[:if]
execute_within_organizer(organizer, options[:if])
end

def permitted_by_unless?(organizer)
return true unless options[:unless]
!execute_within_organizer(organizer, options[:unless])
end

def execute_within_organizer(organizer, symbol_or_proc)
if symbol_or_proc.is_a?(Symbol)
symbol_or_proc.to_proc.call(organizer)
else
organizer.instance_exec(&symbol_or_proc)
end
end
end
end
12 changes: 8 additions & 4 deletions lib/interactor/organizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@ module ClassMethods
# end
#
# Returns nothing.
def organize(*interactors)
organized.concat(interactors.flatten)
def organize(*interactors, **options)
options_dup = options.dup.freeze
organized_interactors = interactors.flatten.map do |interactor|
OrganizedInteractor.new(interactor, options_dup)
end
organized.concat(organized_interactors)
end

# Internal: An Array of declared Interactors to be invoked.
Expand Down Expand Up @@ -76,8 +80,8 @@ module InstanceMethods
#
# Returns nothing.
def call
self.class.organized.each do |interactor|
interactor.call!(context)
self.class.organized.each do |organized_interactor|
organized_interactor.call!(context, self)
end
end
end
Expand Down
35 changes: 35 additions & 0 deletions spec/integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1787,4 +1787,39 @@ def rollback
])
end
end

context "when conditions are passed to organize calls" do
let(:organizer) { build_organizer }

it "regard them while running interactors" do
organizer.class_eval do
def truthy_method
true
end

def falsey_method
false
end
end
organizer.organize(interactor2a, if: :truthy_method)
organizer.organize(interactor2b, if: :falsey_method)
organizer.organize(interactor2c, unless: :truthy_method)
organizer.organize(interactor3, unless: :falsey_method)
organizer.organize(interactor4a, if: -> { truthy_method })
organizer.organize(interactor4b, if: -> { falsey_method })
organizer.organize(interactor4c, unless: -> { truthy_method })
organizer.organize(interactor5, unless: -> { falsey_method })

expect {
organizer.call(context)
}.to change {
context.steps
}.from([]).to([
:around_before2a, :before2a, :call2a, :after2a, :around_after2a,
:around_before3, :before3, :call3, :after3, :around_after3,
:around_before4a, :before4a, :call4a, :after4a, :around_after4a,
:around_before5, :before5, :call5, :after5, :around_after5
])
end
end
end
85 changes: 85 additions & 0 deletions spec/interactor/organized_interactor_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require "ostruct"

module Interactor
describe OrganizedInteractor do
let(:interactor) { double(:interactor, call!: nil) }
let(:organizer) { FakeOrganizer.new }
let(:context) { double(:context) }

class FakeOrganizer
def context
OpenStruct.new(truthy: true, falsey: false)
end

def truthy_method
true
end

def falsey_method
false
end
end

def build_with_options_and_call(options)
OrganizedInteractor.new(interactor, options).call!(context, organizer)
end

describe "#call!" do
it "runs an interactor" do
OrganizedInteractor.new(interactor).call!(context, organizer)
expect(interactor).to have_received(:call!).with(context)
end

context "when :if option is a proc" do
it "evaluates it within an organizer" do
expect(organizer).to receive(:truthy_method).and_call_original
build_with_options_and_call(if: -> { truthy_method })
end

it "runs an interactor if proc evaluation was truthy" do
build_with_options_and_call(if: -> { context.truthy })
expect(interactor).to have_received(:call!)
end

it "doesn't run an interactor if proc evaluation was falsey" do
build_with_options_and_call(if: -> { context.falsey })
expect(interactor).not_to have_received(:call!)
end
end

context "when :unless option is a proc" do
it "evaluates it within an organizer" do
expect(organizer).to receive(:truthy_method).and_call_original
build_with_options_and_call(unless: -> { truthy_method })
end

it "runs an interactor if proc evaluation was falsey" do
build_with_options_and_call(unless: -> { context.falsey })
expect(interactor).to have_received(:call!)
end

it "doesn't run an interactor if proc evaluation was truthy" do
build_with_options_and_call(unless: -> { context.truthy })
expect(interactor).not_to have_received(:call!)
end
end

context "when :if option is a symbol" do
it "treats it as organizer's method name" do
expect(organizer).to receive(:truthy_method).and_call_original
build_with_options_and_call(if: :truthy_method)
end

it "runs an interactor if method evaluation was truthy" do
build_with_options_and_call(if: :truthy_method)
expect(interactor).to have_received(:call!)
end

it "doesn't run an interactor if method evaluation was falsey" do
build_with_options_and_call(if: :falsey_method)
expect(interactor).not_to have_received(:call!)
end
end
end
end
end
47 changes: 32 additions & 15 deletions spec/interactor/organizer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ module Interactor
expect {
organizer.organize(interactor2, interactor3)
}.to change {
organizer.organized
organizer.organized.map(&:interactor)
}.from([]).to([interactor2, interactor3])
end

it "sets interactors given an array of classes" do
expect {
organizer.organize([interactor2, interactor3])
}.to change {
organizer.organized
organizer.organized.map(&:interactor)
}.from([]).to([interactor2, interactor3])
end

Expand All @@ -30,35 +30,52 @@ module Interactor
organizer.organize(interactor2, interactor3)
organizer.organize(interactor4)
}.to change {
organizer.organized
organizer.organized.map(&:interactor)
}.from([]).to([interactor2, interactor3, interactor4])
end

it "passes options to organized interactors" do
expect(OrganizedInteractor).to receive(:new).with(interactor2, if: :foo)
organizer.organize(interactor2, if: :foo)
end

it "duplicates and freezes original options hash" do
original_options = { if: :foo }
expect(OrganizedInteractor).to receive(:new) do |_, passed_options|
expect(passed_options).not_to be(original_options)
expect(passed_options).to be_frozen
end
organizer.organize(interactor2, original_options)
end
end

describe ".organized" do
it "is empty by default" do
expect(organizer.organized).to eq([])
end

it "returns an array of OrganizedInteractors" do
organizer.organize(double(:interactor))
expect(organizer.organized).to all(be_an(OrganizedInteractor))
end
end

describe "#call" do
let(:instance) { organizer.new }
let(:context) { double(:context) }
let(:interactor2) { double(:interactor2) }
let(:interactor3) { double(:interactor3) }
let(:interactor4) { double(:interactor4) }
let(:organized_interactor2) { double(:organized_interactor2) }
let(:organized_interactor3) { double(:organized_interactor3) }

before do
it "calls each organized interactor in order with the context" do
allow(instance).to receive(:context) { context }
allow(organizer).to receive(:organized) {
[interactor2, interactor3, interactor4]
}
end
allow(organizer).to receive(:organized).and_return(
[organized_interactor2, organized_interactor3]
)

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(organized_interactor2)
.to receive(:call!).with(context, instance).ordered
expect(organized_interactor3)
.to receive(:call!).with(context, instance).ordered

instance.call
end
Expand Down

0 comments on commit 1dab0bf

Please sign in to comment.