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.
@@ -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