activeinteractor 1.0.0.beta.3 → 1.0.0.beta.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -1
- data/README.md +28 -4
- data/lib/active_interactor/interactor.rb +25 -7
- data/lib/active_interactor/interactor/perform_options.rb +5 -21
- data/lib/active_interactor/interactor/worker.rb +33 -42
- data/lib/active_interactor/organizer.rb +58 -28
- data/lib/active_interactor/organizer/interactor_interface.rb +66 -0
- data/lib/active_interactor/organizer/interactor_interface_collection.rb +58 -0
- data/lib/active_interactor/version.rb +1 -1
- data/spec/active_interactor/interactor/perform_options_spec.rb +25 -0
- data/spec/active_interactor/interactor/worker_spec.rb +81 -82
- data/spec/active_interactor/organizer/interactor_interface_collection_spec.rb +76 -0
- data/spec/active_interactor/organizer/interactor_interface_spec.rb +162 -0
- data/spec/active_interactor/organizer_spec.rb +36 -32
- data/spec/integration/basic_integration_spec.rb +311 -12
- data/spec/support/shared_examples/a_class_with_interactor_methods_example.rb +4 -4
- metadata +22 -14
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
|
5
|
+
module ActiveInteractor
|
6
|
+
class Organizer < ActiveInteractor::Base
|
7
|
+
# @api private
|
8
|
+
# An interface for an interactor's to allow conditional invokation of an
|
9
|
+
# interactor's {Interactor#perform #perform} method.
|
10
|
+
# @author Aaron Allen <hello@aaronmallen.me>
|
11
|
+
# @since 1.0.0
|
12
|
+
# @!attribute [r] filters
|
13
|
+
# @return [Hash{Symbol=>*}] conditional filters for an interactor's
|
14
|
+
# {Interactor#perform #perform} invocation.
|
15
|
+
# @!attribute [r] interactor_class
|
16
|
+
# @return [Class] an interactor class
|
17
|
+
# @!attribute [r] perform_options
|
18
|
+
# @return [Hash{Symbol=>*}] perform options to use for an interactor's
|
19
|
+
# {Interactor#perform #perform} invocation.
|
20
|
+
class InteractorInterface
|
21
|
+
attr_reader :filters, :interactor_class, :perform_options
|
22
|
+
|
23
|
+
# @return [Array<Symbol>] keywords that indicate an option is a
|
24
|
+
# conditional.
|
25
|
+
CONDITIONAL_FILTERS = %i[if unless].freeze
|
26
|
+
|
27
|
+
# @param interactor_class [Class|Symbol|String] the {.interactor_class}
|
28
|
+
# @param options [Hash{Symbol=>*}] the {.filters} and {.perform_options}
|
29
|
+
# @return [InteractorInterface|nil] a new instance of {InteractorInterface} if
|
30
|
+
# the {#interactor_class} exists
|
31
|
+
def initialize(interactor_class, options = {})
|
32
|
+
@interactor_class = interactor_class.to_s.classify.safe_constantize
|
33
|
+
@filters = options.select { |key, _value| CONDITIONAL_FILTERS.include?(key) }
|
34
|
+
@perform_options = options.reject { |key, _value| CONDITIONAL_FILTERS.include?(key) }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Check conditional filters on an interactor and invoke it's {Interactor#perform #perform}
|
38
|
+
# method if conditions are met.
|
39
|
+
# @param target [Class] an instance of {Organizer}
|
40
|
+
# @param context [Context::Base] the organizer's {Context::Base context} instance
|
41
|
+
# @param fail_on_error [Boolean] if `true` {Interactor::ClassMethods#perform! .perform!}
|
42
|
+
# will be invoked on the interactor. If `false` {Interactor::ClassMethods#perform .perform}
|
43
|
+
# will be invokded on the interactor.
|
44
|
+
# @param perform_options [Hash{Symbol=>*}] options for perform
|
45
|
+
# @return [Context::Base|nil] an instance of {Context::Base} if an interactor's
|
46
|
+
# {Interactor#perform #perform} is invoked
|
47
|
+
def perform(target, context, fail_on_error = false, perform_options = {})
|
48
|
+
return if check_conditionals(target, filters[:if]) == false
|
49
|
+
return if check_conditionals(target, filters[:unless]) == true
|
50
|
+
|
51
|
+
method = fail_on_error ? :perform! : :perform
|
52
|
+
options = self.perform_options.merge(perform_options)
|
53
|
+
interactor_class.send(method, context, options)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def check_conditionals(target, filter)
|
59
|
+
return unless filter
|
60
|
+
|
61
|
+
return target.send(filter) if filter.is_a?(Symbol)
|
62
|
+
return target.instance_exec(&filter) if filter.is_a?(Proc)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_interactor/organizer/interactor_interface'
|
4
|
+
|
5
|
+
module ActiveInteractor
|
6
|
+
class Organizer < ActiveInteractor::Base
|
7
|
+
# @api private
|
8
|
+
# A collection of ordered interactors for an {Organizer}
|
9
|
+
# @author Aaron Allen <hello@aaronmallen.me>
|
10
|
+
# @since 1.0.0
|
11
|
+
# @!attribute [r] collection
|
12
|
+
# @return [Array<Hash{Symbol=>*}] the organized interactors and filters
|
13
|
+
class InteractorInterfaceCollection
|
14
|
+
attr_reader :collection
|
15
|
+
|
16
|
+
# @!method each(&block)
|
17
|
+
# Calls the given block once for each element of {#collection}, passing that
|
18
|
+
# element as a parameter
|
19
|
+
# @yield [.collection] the {#collection}
|
20
|
+
# @return [Array<InteractorInterface>] the {#collection}
|
21
|
+
|
22
|
+
# @!method map(&block)
|
23
|
+
# Invokes the given block once fore each element of {#collection}.
|
24
|
+
# @yield [.collection] the {#collection}
|
25
|
+
# @return [Array] a new array containing the values returned by the block.
|
26
|
+
delegate :each, :map, to: :collection
|
27
|
+
|
28
|
+
# @return [InteractorInterfaceCollection] a new instance of {InteractorInterfaceCollection}
|
29
|
+
def initialize
|
30
|
+
@collection = []
|
31
|
+
end
|
32
|
+
|
33
|
+
# Add an {InteractorInterface} to the {#collection}
|
34
|
+
# @param interactor [Class|Symbol|String] an interactor class name
|
35
|
+
# @param filters [Hash{Symbol=>*}] conditions and {Interactor::PerformOptions} for the interactor
|
36
|
+
# @option filters [Symbol|Proc] :if only invoke the interactor's perform method
|
37
|
+
# if method or block returns `true`
|
38
|
+
# @option filters [Symbol|Proc] :unless only invoke the interactor's perform method
|
39
|
+
# if method or block returns `false`
|
40
|
+
# @see Interactor::PerformOptions
|
41
|
+
# @return [InteractorInterface] the {InteractorInterface} instance
|
42
|
+
def add(interactor, filters = {})
|
43
|
+
interface = InteractorInterface.new(interactor, filters)
|
44
|
+
collection << interface if interface.interactor_class
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# Concat multiple {InteractorInterface} to the {#collection}
|
49
|
+
# @param interactors [Array<Class>] the interactor classes to add
|
50
|
+
# to the collection
|
51
|
+
# @return [InteractorInterface] the {InteractorInterface} instance
|
52
|
+
def concat(interactors)
|
53
|
+
interactors.flatten.each { |interactor| add(interactor) }
|
54
|
+
self
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe ActiveInteractor::Interactor::PerformOptions do
|
6
|
+
subject { described_class.new }
|
7
|
+
|
8
|
+
it { is_expected.to respond_to :skip_each_perform_callbacks }
|
9
|
+
it { is_expected.to respond_to :skip_perform_callbacks }
|
10
|
+
it { is_expected.to respond_to :skip_rollback }
|
11
|
+
it { is_expected.to respond_to :skip_rollback_callbacks }
|
12
|
+
it { is_expected.to respond_to :validate }
|
13
|
+
it { is_expected.to respond_to :validate_on_calling }
|
14
|
+
it { is_expected.to respond_to :validate_on_called }
|
15
|
+
|
16
|
+
describe 'defaults' do
|
17
|
+
it { is_expected.to have_attributes(skip_each_perform_callbacks: false) }
|
18
|
+
it { is_expected.to have_attributes(skip_perform_callbacks: false) }
|
19
|
+
it { is_expected.to have_attributes(skip_rollback: false) }
|
20
|
+
it { is_expected.to have_attributes(skip_rollback_callbacks: false) }
|
21
|
+
it { is_expected.to have_attributes(validate: true) }
|
22
|
+
it { is_expected.to have_attributes(validate_on_calling: true) }
|
23
|
+
it { is_expected.to have_attributes(validate_on_called: true) }
|
24
|
+
end
|
25
|
+
end
|
@@ -8,6 +8,72 @@ RSpec.describe ActiveInteractor::Interactor::Worker do
|
|
8
8
|
before { build_interactor }
|
9
9
|
let(:interactor) { TestInteractor.new }
|
10
10
|
|
11
|
+
RSpec.shared_examples 'an interactor with options' do
|
12
|
+
context 'when interactor has options :skip_perform_callbacks eq to true' do
|
13
|
+
let(:interactor) { TestInteractor.new.with_options(skip_perform_callbacks: true) }
|
14
|
+
|
15
|
+
it 'is expected not to invoke #run_callbacks with :perform' do
|
16
|
+
allow_any_instance_of(TestInteractor).to receive(:run_callbacks)
|
17
|
+
.with(:validation).and_call_original
|
18
|
+
expect_any_instance_of(TestInteractor).not_to receive(:run_callbacks)
|
19
|
+
.with(:perform)
|
20
|
+
subject
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'when interactor has options :validate eq to false' do
|
25
|
+
let(:interactor) { TestInteractor.new.with_options(validate: false) }
|
26
|
+
|
27
|
+
it 'is expected not to invoke #run_callbacks with :validation' do
|
28
|
+
expect_any_instance_of(TestInteractor).not_to receive(:run_callbacks)
|
29
|
+
.with(:validation)
|
30
|
+
subject
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'when interactor has options :validate_on_calling eq to false' do
|
35
|
+
let(:interactor) { TestInteractor.new.with_options(validate_on_calling: false) }
|
36
|
+
|
37
|
+
before do
|
38
|
+
allow_any_instance_of(TestInteractor).to receive(:context_valid?)
|
39
|
+
.with(:called).and_return(true)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'is expected not to invoke #context_valid? with :calling' do
|
43
|
+
expect_any_instance_of(TestInteractor).not_to receive(:context_valid?)
|
44
|
+
.with(:calling)
|
45
|
+
subject
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'is expected to invoke #context_valid? with :called' do
|
49
|
+
expect_any_instance_of(TestInteractor).to receive(:context_valid?)
|
50
|
+
.with(:called)
|
51
|
+
subject
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when interactor has options :validate_on_called eq to false' do
|
56
|
+
let(:interactor) { TestInteractor.new.with_options(validate_on_called: false) }
|
57
|
+
|
58
|
+
before do
|
59
|
+
allow_any_instance_of(TestInteractor).to receive(:context_valid?)
|
60
|
+
.with(:calling).and_return(true)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'is expected to invoke #context_valid? with :calling' do
|
64
|
+
expect_any_instance_of(TestInteractor).to receive(:context_valid?)
|
65
|
+
.with(:calling)
|
66
|
+
subject
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'is expected not to invoke #context_valid? with :called' do
|
70
|
+
expect_any_instance_of(TestInteractor).not_to receive(:context_valid?)
|
71
|
+
.with(:called)
|
72
|
+
subject
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
11
77
|
describe '#execute_perform' do
|
12
78
|
subject { described_class.new(interactor).execute_perform }
|
13
79
|
|
@@ -22,6 +88,8 @@ RSpec.describe ActiveInteractor::Interactor::Worker do
|
|
22
88
|
it { expect { subject }.not_to raise_error }
|
23
89
|
it { is_expected.to be_an TestInteractor.context_class }
|
24
90
|
end
|
91
|
+
|
92
|
+
include_examples 'an interactor with options'
|
25
93
|
end
|
26
94
|
|
27
95
|
describe '#execute_perform!' do
|
@@ -40,23 +108,6 @@ RSpec.describe ActiveInteractor::Interactor::Worker do
|
|
40
108
|
subject
|
41
109
|
end
|
42
110
|
|
43
|
-
context 'with options :skip_perform_callbacks eq to true' do
|
44
|
-
subject { described_class.new(interactor).execute_perform!(skip_perform_callbacks: true) }
|
45
|
-
|
46
|
-
it 'is expected not to run perform callbacks on interactor' do
|
47
|
-
allow_any_instance_of(TestInteractor).to receive(:run_callbacks)
|
48
|
-
.with(:validation).and_call_original
|
49
|
-
expect_any_instance_of(TestInteractor).not_to receive(:run_callbacks)
|
50
|
-
.with(:perform)
|
51
|
-
subject
|
52
|
-
end
|
53
|
-
|
54
|
-
it 'calls #perform on interactor instance' do
|
55
|
-
expect_any_instance_of(TestInteractor).to receive(:perform)
|
56
|
-
subject
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
111
|
context 'when interactor context is invalid on :calling' do
|
61
112
|
before do
|
62
113
|
allow_any_instance_of(TestInteractor.context_class).to receive(:valid?)
|
@@ -72,33 +123,6 @@ RSpec.describe ActiveInteractor::Interactor::Worker do
|
|
72
123
|
expect_any_instance_of(TestInteractor).to receive(:context_rollback!)
|
73
124
|
expect { subject }.to raise_error(ActiveInteractor::Error::ContextFailure)
|
74
125
|
end
|
75
|
-
|
76
|
-
context 'with options :validate eq to false' do
|
77
|
-
subject { described_class.new(interactor).execute_perform!(validate: false) }
|
78
|
-
|
79
|
-
it { expect { subject }.not_to raise_error }
|
80
|
-
it 'is expected not to run validation callbacks on interactor' do
|
81
|
-
expect_any_instance_of(TestInteractor).not_to receive(:run_callbacks)
|
82
|
-
.with(:validation)
|
83
|
-
subject
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
context 'with options :validate_on_calling eq to false' do
|
88
|
-
subject { described_class.new(interactor).execute_perform!(validate_on_calling: false) }
|
89
|
-
|
90
|
-
it { expect { subject }.not_to raise_error }
|
91
|
-
it 'is expected not to call valid? with :calling' do
|
92
|
-
expect_any_instance_of(TestInteractor.context_class).not_to receive(:valid?)
|
93
|
-
.with(:calling)
|
94
|
-
subject
|
95
|
-
end
|
96
|
-
it 'is expected to call valid? with :called' do
|
97
|
-
expect_any_instance_of(TestInteractor.context_class).to receive(:valid?)
|
98
|
-
.with(:called)
|
99
|
-
subject
|
100
|
-
end
|
101
|
-
end
|
102
126
|
end
|
103
127
|
|
104
128
|
context 'when interactor context is invalid on :called' do
|
@@ -116,34 +140,9 @@ RSpec.describe ActiveInteractor::Interactor::Worker do
|
|
116
140
|
expect_any_instance_of(TestInteractor).to receive(:context_rollback!)
|
117
141
|
expect { subject }.to raise_error(ActiveInteractor::Error::ContextFailure)
|
118
142
|
end
|
119
|
-
|
120
|
-
context 'with options :validate eq to false' do
|
121
|
-
subject { described_class.new(interactor).execute_perform!(validate: false) }
|
122
|
-
|
123
|
-
it { expect { subject }.not_to raise_error }
|
124
|
-
it 'is expected not to run validation callbacks on interactor' do
|
125
|
-
expect_any_instance_of(TestInteractor).not_to receive(:run_callbacks)
|
126
|
-
.with(:validation)
|
127
|
-
subject
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
context 'with options :validate_on_called eq to false' do
|
132
|
-
subject { described_class.new(interactor).execute_perform!(validate_on_called: false) }
|
133
|
-
|
134
|
-
it { expect { subject }.not_to raise_error }
|
135
|
-
it 'is expected to call valid? with :calling' do
|
136
|
-
expect_any_instance_of(TestInteractor.context_class).to receive(:valid?)
|
137
|
-
.with(:calling)
|
138
|
-
subject
|
139
|
-
end
|
140
|
-
it 'is expected not to call valid? with :called' do
|
141
|
-
expect_any_instance_of(TestInteractor.context_class).not_to receive(:valid?)
|
142
|
-
.with(:called)
|
143
|
-
subject
|
144
|
-
end
|
145
|
-
end
|
146
143
|
end
|
144
|
+
|
145
|
+
include_examples 'an interactor with options'
|
147
146
|
end
|
148
147
|
|
149
148
|
describe '#execute_rollback' do
|
@@ -155,31 +154,31 @@ RSpec.describe ActiveInteractor::Interactor::Worker do
|
|
155
154
|
subject
|
156
155
|
end
|
157
156
|
|
158
|
-
it '
|
157
|
+
it 'is expected to invoke #context_rollback on interactor instance' do
|
159
158
|
expect_any_instance_of(TestInteractor).to receive(:context_rollback!)
|
160
159
|
subject
|
161
160
|
end
|
162
161
|
|
163
|
-
context '
|
164
|
-
|
162
|
+
context 'when interactor has options :skip_rollback eq to true' do
|
163
|
+
let(:interactor) { TestInteractor.new.with_options(skip_rollback: true) }
|
165
164
|
|
166
|
-
it 'is expected not to
|
165
|
+
it 'is expected not to invoke #context_rollback on interactor instance' do
|
167
166
|
expect_any_instance_of(TestInteractor).not_to receive(:context_rollback!)
|
168
167
|
subject
|
169
168
|
end
|
170
169
|
end
|
171
170
|
|
172
|
-
context '
|
173
|
-
|
171
|
+
context 'when interactor has options :skip_rollback_callbacks eq to true' do
|
172
|
+
let(:interactor) { TestInteractor.new.with_options(skip_rollback_callbacks: true) }
|
174
173
|
|
175
|
-
it 'is expected
|
176
|
-
expect_any_instance_of(TestInteractor).
|
177
|
-
.with(:rollback)
|
174
|
+
it 'is expected to invoke #context_rollback on interactor instance' do
|
175
|
+
expect_any_instance_of(TestInteractor).to receive(:context_rollback!)
|
178
176
|
subject
|
179
177
|
end
|
180
178
|
|
181
|
-
it '
|
182
|
-
expect_any_instance_of(TestInteractor).
|
179
|
+
it 'is expected not to run rollback callbacks on interactor' do
|
180
|
+
expect_any_instance_of(TestInteractor).not_to receive(:run_callbacks)
|
181
|
+
.with(:rollback)
|
183
182
|
subject
|
184
183
|
end
|
185
184
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe ActiveInteractor::Organizer::InteractorInterfaceCollection do
|
6
|
+
describe '#add' do
|
7
|
+
subject { instance.add(interactor) }
|
8
|
+
let(:instance) { described_class.new }
|
9
|
+
|
10
|
+
context 'with an interactor that does not exist' do
|
11
|
+
let(:interactor) { :an_interactor_that_does_not_exist }
|
12
|
+
|
13
|
+
it { expect { subject }.not_to(change { instance.collection.count }) }
|
14
|
+
it { is_expected.to be_a described_class }
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with an existing interactor' do
|
18
|
+
before { build_interactor }
|
19
|
+
|
20
|
+
context 'when interactors are passed as contants' do
|
21
|
+
let(:interactor) { TestInteractor }
|
22
|
+
|
23
|
+
it { expect { subject }.to change { instance.collection.count }.by(1) }
|
24
|
+
it { is_expected.to be_a described_class }
|
25
|
+
|
26
|
+
it 'is expected to add the appropriate interactor' do
|
27
|
+
subject
|
28
|
+
expect(instance.collection.first.interactor_class).to eq TestInteractor
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'when interactors are passed as symbols' do
|
33
|
+
let(:interactor) { :test_interactor }
|
34
|
+
|
35
|
+
it { expect { subject }.to change { instance.collection.count }.by(1) }
|
36
|
+
it { is_expected.to be_a described_class }
|
37
|
+
|
38
|
+
it 'is expected to add the appropriate interactor' do
|
39
|
+
subject
|
40
|
+
expect(instance.collection.first.interactor_class).to eq TestInteractor
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'when interactors are passed as strings' do
|
45
|
+
let(:interactor) { 'TestInteractor' }
|
46
|
+
|
47
|
+
it { expect { subject }.to change { instance.collection.count }.by(1) }
|
48
|
+
it { is_expected.to be_a described_class }
|
49
|
+
|
50
|
+
it 'is expected to add the appropriate interactor' do
|
51
|
+
subject
|
52
|
+
expect(instance.collection.first.interactor_class).to eq TestInteractor
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#concat' do
|
59
|
+
subject { instance.concat(interactors) }
|
60
|
+
let(:instance) { described_class.new }
|
61
|
+
|
62
|
+
context 'with two existing interactors' do
|
63
|
+
let!(:interactor1) { build_interactor('TestInteractor1') }
|
64
|
+
let!(:interactor2) { build_interactor('TestInteractor2') }
|
65
|
+
let(:interactors) { %i[test_interactor_1 test_interactor_2] }
|
66
|
+
|
67
|
+
it { expect { subject }.to change { instance.collection.count }.by(2) }
|
68
|
+
|
69
|
+
it 'is expected to add the appropriate interactors' do
|
70
|
+
subject
|
71
|
+
expect(instance.collection.first.interactor_class).to eq TestInteractor1
|
72
|
+
expect(instance.collection.last.interactor_class).to eq TestInteractor2
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|