activeinteractor 1.0.0.beta.3 → 1.0.0.beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|