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.
@@ -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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ActiveInteractor
4
4
  # @return [String] the ActiveInteractor version
5
- VERSION = '1.0.0.beta.3'
5
+ VERSION = '1.0.0.beta.4'
6
6
  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 'calls #context_rollback on interactor instance' do
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 'with options :skip_rollback eq to true' do
164
- subject { described_class.new(interactor).execute_rollback(skip_rollback: true) }
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 call #context_rollback on interactor instance' do
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 'with options :skip_rollback_callbacks eq to true' do
173
- subject { described_class.new(interactor).execute_rollback(skip_rollback_callbacks: true) }
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 not to run rollback callbacks on interactor' do
176
- expect_any_instance_of(TestInteractor).not_to receive(:run_callbacks)
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 'calls #context_rollback on interactor instance' do
182
- expect_any_instance_of(TestInteractor).to receive(:context_rollback!)
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