activeinteractor 1.0.5 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 491bd06fcc2f9baf372f817dab6e40c69a07d5ff15721f5136815bcdeb04656b
4
- data.tar.gz: 80e520f97369ab0f9465842eb31f9b0c0e638135cdc73ee27ded9cdc12272cf1
3
+ metadata.gz: bbabe89a7d29d7b1faba5a48f6d7cc105db0bc783ce6b14ecef236534a69c2e2
4
+ data.tar.gz: 6716e1bba69f443f360632aa8af7f3c5c89f3b60d5bf3679427e63c12d65b454
5
5
  SHA512:
6
- metadata.gz: 3409d7c2a1c81fcfa6482493569a015356f6239635b4ea539947687c2e2ba44947825b8914ee264df7280e7f612184b35ae67cc10010a6df793087f249aacc81
7
- data.tar.gz: dd892f0304bd572a8074c56cad88a231a9ca328b6365ffb96cf87ea7c60c5121f11ed32c3de70a550e3d9ea7fdeb5489667b653041e41c64e90dd661ea35d5e7
6
+ metadata.gz: 34bbeb89139ffdccb6a1fc38e73f125995a21a9ad2026bbde21ac6a251ff0bec533d35143ce13e569fe2f55c33feafb03e8eac142b2e1577d127f359b4b52d4c
7
+ data.tar.gz: dc8e8775074d0d451678597c459ed6bc4b55043c89755c6be0d34ca56ab4cf74624686bad25e4111e7457fed5f0bcd3ee8d3cab6316b4d5a13567b8ac004e6ad
@@ -7,11 +7,23 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [v1.1.0] - 2020-10-04
11
+
12
+ ### Added
13
+
14
+ - [\#247](https://github.com/aaronmallen/activeinteractor/issues/247) Support in place callbacks
15
+
16
+ ### Fixed
17
+
18
+ - [\#242](https://github.com/aaronmallen/activeinteractor/issues/242) Optional attributes are always null
19
+ - [\#243](https://github.com/aaronmallen/activeinteractor/issues/243) Nested Organizers do not rollback parent context
20
+
10
21
  ## [v1.0.5] - 2020-09-15
11
22
 
12
23
  ### Fixed
13
24
 
14
- - [\#200](https://github.com/aaronmallen/activeinteractor/issues/200) Context attributes assigned in interactor not accessible as element within interactor
25
+ - [\#200](https://github.com/aaronmallen/activeinteractor/issues/200) Context attributes assigned in interactor not
26
+ accessible as element within interactor
15
27
 
16
28
  ## [v1.0.4] - 2020-02-11
17
29
 
@@ -200,7 +212,9 @@ and this project adheres to [Semantic Versioning].
200
212
 
201
213
  <!-- versions -->
202
214
 
203
- [Unreleased]: https://github.com/aaronmallen/activeinteractor/compare/v1.0.4...HEAD
215
+ [Unreleased]: https://github.com/aaronmallen/activeinteractor/compare/v1.1.0...HEAD
216
+ [v1.1.0]: https://github.com/aaronmallen/activeinteractor/compare/v1.0.5...v1.1.0
217
+ [v1.0.5]: https://github.com/aaronmallen/activeinteractor/compare/v1.0.4...v1.0.5
204
218
  [v1.0.4]: https://github.com/aaronmallen/activeinteractor/compare/v1.0.3...v1.0.4
205
219
  [v1.0.3]: https://github.com/aaronmallen/activeinteractor/compare/v1.0.2...v1.0.3
206
220
  [v1.0.2]: https://github.com/aaronmallen/activeinteractor/compare/v1.0.1...v1.0.2
@@ -69,6 +69,19 @@ module ActiveInteractor
69
69
  @table[name.to_sym] || attributes[name.to_sym]
70
70
  end
71
71
 
72
+ # Sets value of a Hash attribute in context.attributes
73
+ #
74
+ # @since 1.1.0
75
+ #
76
+ # @param name [String, Symbol] the key name of the attribute
77
+ # @param value [*] the value to be given attribute name
78
+ # @returns [*] the attribute value
79
+ def []=(name, value)
80
+ public_send("#{name}=", value)
81
+
82
+ super
83
+ end
84
+
72
85
  # Get values defined on the instance of {Base context} whose keys are defined on the {Base context} class'
73
86
  # {ClassMethods#attributes .attributes}
74
87
  #
@@ -117,7 +130,8 @@ module ActiveInteractor
117
130
  def merge!(context)
118
131
  merge_errors!(context) if context.respond_to?(:errors)
119
132
  copy_flags!(context)
120
- context.each_pair do |key, value|
133
+
134
+ merged_context_attributes(context).each_pair do |key, value|
121
135
  public_send("#{key}=", value) unless value.nil?
122
136
  end
123
137
  self
@@ -129,6 +143,13 @@ module ActiveInteractor
129
143
  @_called ||= []
130
144
  end
131
145
 
146
+ def merged_context_attributes(context)
147
+ attrs = {}
148
+ attrs.merge!(context.to_h) if context.respond_to?(:to_h)
149
+ attrs.merge!(context.attributes.to_h) if context.respond_to?(:attributes)
150
+ attrs
151
+ end
152
+
132
153
  def context_attributes_as_hash(context)
133
154
  return context.to_h if context&.respond_to?(:to_h)
134
155
  return context.attributes.to_h if context.respond_to?(:attributes)
@@ -14,6 +14,13 @@ module ActiveInteractor
14
14
  #
15
15
  # @return [Hash{Symbol=>Proc, Symbol}] conditional options for the {ActiveInteractor::Base interactor} class
16
16
  #
17
+ # @!attribute [r] callbacks
18
+ # Callbacks for the interactor_class
19
+ #
20
+ # @since 1.1.0
21
+ #
22
+ # @return [Hash{Symbol=>*}] the interactor callbacks
23
+ #
17
24
  # @!attribute [r] interactor_class
18
25
  # An {ActiveInteractor::Base interactor} class
19
26
  #
@@ -27,11 +34,12 @@ module ActiveInteractor
27
34
  # @return [Hash{Symbol=>*}] {Interactor::Perform::Options} for the {ActiveInteractor::Base interactor}
28
35
  # {Interactor::Perform#perform #perform}
29
36
  class InteractorInterface
30
- attr_reader :filters, :interactor_class, :perform_options
37
+ attr_reader :filters, :callbacks, :interactor_class, :perform_options
31
38
 
32
39
  # Keywords for conditional filters
33
40
  # @return [Array<Symbol>]
34
41
  CONDITIONAL_FILTERS = %i[if unless].freeze
42
+ CALLBACKS = %i[before after].freeze
35
43
 
36
44
  # Initialize a new instance of {InteractorInterface}
37
45
  #
@@ -42,7 +50,8 @@ module ActiveInteractor
42
50
  def initialize(interactor_class, options = {})
43
51
  @interactor_class = interactor_class.to_s.camelize.safe_constantize
44
52
  @filters = options.select { |key, _value| CONDITIONAL_FILTERS.include?(key) }
45
- @perform_options = options.reject { |key, _value| CONDITIONAL_FILTERS.include?(key) }
53
+ @callbacks = options.select { |key, _value| CALLBACKS.include?(key) }
54
+ @perform_options = options.reject { |key, _value| CONDITIONAL_FILTERS.include?(key) || CALLBACKS.include?(key) }
46
55
  end
47
56
 
48
57
  # Call the {#interactor_class} {Interactor::Perform::ClassMethods#perform .perform} or
@@ -57,21 +66,29 @@ module ActiveInteractor
57
66
  # {Context::Status#fail! fails} its {Context::Base context}.
58
67
  # @return [Class] an instance of {Context::Base context}
59
68
  def perform(target, context, fail_on_error = false, perform_options = {})
60
- return if check_conditionals(target, filters[:if]) == false
61
- return if check_conditionals(target, filters[:unless]) == true
69
+ return if check_conditionals(target, :if) == false
70
+ return if check_conditionals(target, :unless) == true
62
71
 
63
72
  method = fail_on_error ? :perform! : :perform
64
73
  options = self.perform_options.merge(perform_options)
65
74
  interactor_class.send(method, context, options)
66
75
  end
67
76
 
77
+ def execute_inplace_callback(target, callback)
78
+ resolve_option(target, callbacks[callback])
79
+ end
80
+
68
81
  private
69
82
 
70
83
  def check_conditionals(target, filter)
71
- return unless filter
84
+ resolve_option(target, filters[filter])
85
+ end
86
+
87
+ def resolve_option(target, opt)
88
+ return unless opt
72
89
 
73
- return target.send(filter) if filter.is_a?(Symbol)
74
- return target.instance_exec(&filter) if filter.is_a?(Proc)
90
+ return target.send(opt) if opt.is_a?(Symbol)
91
+ return target.instance_exec(&opt) if opt.is_a?(Proc)
75
92
  end
76
93
  end
77
94
  end
@@ -73,10 +73,19 @@ module ActiveInteractor
73
73
  context_fail! if contexts.any?(&:failure?)
74
74
  end
75
75
 
76
+ def execute_and_merge_contexts(interface)
77
+ interface.execute_inplace_callback(self, :before)
78
+ result = execute_interactor_with_callbacks(interface, true)
79
+ return if result.nil?
80
+
81
+ context.merge!(result)
82
+ context_fail! if result.failure?
83
+ interface.execute_inplace_callback(self, :after)
84
+ end
85
+
76
86
  def perform_in_order
77
87
  self.class.organized.each do |interface|
78
- result = execute_interactor_with_callbacks(interface, true)
79
- context.merge!(result) if result
88
+ execute_and_merge_contexts(interface)
80
89
  end
81
90
  rescue ActiveInteractor::Error::ContextFailure => e
82
91
  context.merge!(e.context)
@@ -3,5 +3,5 @@
3
3
  module ActiveInteractor
4
4
  # The ActiveInteractor version
5
5
  # @return [String] the ActiveInteractor version
6
- VERSION = '1.0.5'
6
+ VERSION = '1.1.0'
7
7
  end
@@ -84,6 +84,72 @@ RSpec.describe ActiveInteractor::Organizer::InteractorInterface do
84
84
  end
85
85
  end
86
86
 
87
+ context 'with options {:before => :some_method }' do
88
+ let(:options) { { before: :some_method } }
89
+
90
+ describe '#callbacks' do
91
+ subject { instance.callbacks }
92
+
93
+ it { is_expected.to eq(before: :some_method) }
94
+ end
95
+
96
+ describe '#perform_options' do
97
+ subject { instance.perform_options }
98
+
99
+ it { is_expected.to be_empty }
100
+ end
101
+ end
102
+
103
+ context 'with options {:before => -> { context.test = true } }' do
104
+ let(:options) { { before: -> { context.test = true } } }
105
+
106
+ describe '#callbacks' do
107
+ subject { instance.callbacks }
108
+
109
+ it { expect(subject[:before]).not_to be_nil }
110
+ it { expect(subject[:before]).to be_a Proc }
111
+ end
112
+
113
+ describe '#perform_options' do
114
+ subject { instance.perform_options }
115
+
116
+ it { is_expected.to be_empty }
117
+ end
118
+ end
119
+
120
+ context 'with options {:after => :some_method }' do
121
+ let(:options) { { after: :some_method } }
122
+
123
+ describe '#callbacks' do
124
+ subject { instance.callbacks }
125
+
126
+ it { is_expected.to eq(after: :some_method) }
127
+ end
128
+
129
+ describe '#perform_options' do
130
+ subject { instance.perform_options }
131
+
132
+ it { is_expected.to be_empty }
133
+ end
134
+ end
135
+
136
+ context 'with options {:after => -> { context.test = true } }' do
137
+ let(:options) { { after: -> { context.test = true } } }
138
+
139
+ describe '#callbacks' do
140
+ subject { instance.callbacks }
141
+
142
+ it { expect(subject[:after]).not_to be_nil }
143
+ it { expect(subject[:after]).to be_a Proc }
144
+ end
145
+
146
+ describe '#perform_options' do
147
+ subject { instance.perform_options }
148
+
149
+ it { is_expected.to be_empty }
150
+ end
151
+ end
152
+
87
153
  context 'with options { :validate => false }' do
88
154
  let(:options) { { validate: false } }
89
155
 
@@ -100,8 +166,8 @@ RSpec.describe ActiveInteractor::Organizer::InteractorInterface do
100
166
  end
101
167
  end
102
168
 
103
- context 'with options { :if => :some_method, :validate => false }' do
104
- let(:options) { { if: :some_method, validate: false } }
169
+ context 'with options { :if => :some_method, :validate => false, :before => :other_method }' do
170
+ let(:options) { { if: :some_method, validate: false, before: :other_method } }
105
171
 
106
172
  describe '#filters' do
107
173
  subject { instance.filters }
@@ -109,6 +175,12 @@ RSpec.describe ActiveInteractor::Organizer::InteractorInterface do
109
175
  it { is_expected.to eq(if: :some_method) }
110
176
  end
111
177
 
178
+ describe '#callbacks' do
179
+ subject { instance.callbacks }
180
+
181
+ it { is_expected.to eq(before: :other_method) }
182
+ end
183
+
112
184
  describe '#perform_options' do
113
185
  subject { instance.perform_options }
114
186
 
@@ -140,6 +140,8 @@ RSpec.describe 'A basic organizer', type: :integration do
140
140
  let!(:test_context_class) do
141
141
  build_context('TestInteractor1Context') do
142
142
  attributes :foo
143
+ attributes :baz
144
+ attributes :zoo
143
145
  end
144
146
  end
145
147
 
@@ -151,6 +153,12 @@ RSpec.describe 'A basic organizer', type: :integration do
151
153
  context.has_foo_as_element = context[:foo].present?
152
154
  context.has_bar_as_method = context.bar.present?
153
155
  context.has_bar_as_element = context[:bar].present?
156
+ context.baz = 'baz'
157
+ context.has_baz_as_method = context.baz.present?
158
+ context.has_baz_as_element = context[:baz].present?
159
+ context[:zoo] = 'zoo'
160
+ context.has_zoo_as_method = context.zoo.present?
161
+ context.has_zoo_as_element = context[:zoo].present?
154
162
  end
155
163
  end
156
164
  end
@@ -167,13 +175,17 @@ RSpec.describe 'A basic organizer', type: :integration do
167
175
 
168
176
  describe '.perform' do
169
177
  subject(:result) { interactor_class.perform(context_attributes) }
170
- it { is_expected.to have_attributes(foo: 'foo', bar: 'bar') }
178
+ it { is_expected.to have_attributes(foo: 'foo', bar: 'bar', baz: 'baz', zoo: 'zoo') }
171
179
 
172
180
  it 'is expected to copy all attributes in the contexts to each interactor' do
173
181
  expect(subject.has_foo_as_method).to be true
174
182
  expect(subject.has_foo_as_element).to be true
175
183
  expect(subject.has_bar_as_method).to be true
176
184
  expect(subject.has_bar_as_element).to be true
185
+ expect(subject.has_baz_as_method).to be true
186
+ expect(subject.has_baz_as_element).to be true
187
+ expect(subject.has_zoo_as_method).to be true
188
+ expect(subject.has_zoo_as_element).to be true
177
189
  end
178
190
 
179
191
  describe '#attributes' do
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'An organizer with failing nested organizer', type: :integration do
6
+ let!(:parent_interactor1) { build_interactor('TestParentInteractor1') }
7
+ let!(:parent_interactor2) { build_interactor('TestParentInteractor2') }
8
+ let!(:child_interactor1) { build_interactor('TestChildInteractor1') }
9
+ let!(:child_interactor2) do
10
+ build_interactor('TestChildInteractor2') do
11
+ def perform
12
+ context.fail!
13
+ end
14
+ end
15
+ end
16
+
17
+ let!(:child_interactor_class) do
18
+ build_organizer('TestChildOrganizer') do
19
+ organize TestChildInteractor1, TestChildInteractor2
20
+ end
21
+ end
22
+
23
+ let(:parent_interactor_class) do
24
+ build_organizer('TestParentOrganizer') do
25
+ organize TestParentInteractor1, TestChildOrganizer, TestParentInteractor2
26
+ end
27
+ end
28
+
29
+ describe '.perform' do
30
+ subject { parent_interactor_class.perform }
31
+
32
+ before do
33
+ expect_any_instance_of(child_interactor_class).to receive(:perform).exactly(:once).and_call_original
34
+ expect_any_instance_of(child_interactor1).to receive(:perform).exactly(:once).and_call_original
35
+ expect_any_instance_of(child_interactor2).to receive(:perform).exactly(:once).and_call_original
36
+ expect_any_instance_of(child_interactor2).to receive(:rollback).exactly(:once).and_call_original
37
+ expect_any_instance_of(child_interactor1).to receive(:rollback).exactly(:once).and_call_original
38
+ expect_any_instance_of(parent_interactor1).to receive(:rollback).exactly(:once).and_call_original
39
+ expect_any_instance_of(parent_interactor2).not_to receive(:perform).exactly(:once).and_call_original
40
+ expect_any_instance_of(parent_interactor2).not_to receive(:rollback).exactly(:once).and_call_original
41
+ end
42
+
43
+ it { is_expected.to be_a parent_interactor_class.context_class }
44
+
45
+ it { is_expected.to be_failure }
46
+ end
47
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'An organizer with options callbacks', type: :integration do
6
+ let!(:interactor1) do
7
+ build_interactor('TestInteractor1') do
8
+ def perform
9
+ context.step = 3 if context.step == 2
10
+ context.step = 6 if context.step == 5
11
+ end
12
+ end
13
+ end
14
+
15
+ let(:interactor_class) do
16
+ build_organizer do
17
+ organize do
18
+ add TestInteractor1, before: :test_before_method, after: :test_after_method
19
+ add TestInteractor1, before: lambda {
20
+ context.step = 5 if context.step == 4
21
+ }, after: lambda {
22
+ context.step = 7 if context.step == 6
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ def test_before_method
29
+ context.step = 2 if context.step == 1
30
+ end
31
+
32
+ def test_after_method
33
+ context.step = 4 if context.step == 3
34
+ end
35
+ end
36
+ end
37
+
38
+ include_examples 'a class with interactor methods'
39
+ include_examples 'a class with interactor callback methods'
40
+ include_examples 'a class with interactor context methods'
41
+ include_examples 'a class with organizer callback methods'
42
+
43
+ describe '.perform' do
44
+ subject { interactor_class.perform(step: 1) }
45
+
46
+ it { is_expected.to be_a interactor_class.context_class }
47
+ it 'is expected to receive #test_before_method once' do
48
+ expect_any_instance_of(interactor_class).to receive(:test_before_method)
49
+ .exactly(:once)
50
+ subject
51
+ end
52
+
53
+ it 'is expected to receive #test_after_method once' do
54
+ expect_any_instance_of(interactor_class).to receive(:test_after_method)
55
+ .exactly(:once)
56
+ subject
57
+ end
58
+
59
+ it 'runs callbacks in sequence' do
60
+ expect(subject.step).to eq(7)
61
+ end
62
+ end
63
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeinteractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.5
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Allen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-16 00:00:00.000000000 Z
11
+ date: 2020-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -188,6 +188,8 @@ files:
188
188
  - spec/integration/an_organizer_with_around_each_callbacks_spec.rb
189
189
  - spec/integration/an_organizer_with_before_each_callbacks_spec.rb
190
190
  - spec/integration/an_organizer_with_conditionally_organized_interactors_spec.rb
191
+ - spec/integration/an_organizer_with_failing_nested_organizer_spec.rb
192
+ - spec/integration/an_organizer_with_options_callbacks_spec.rb
191
193
  - spec/spec_helper.rb
192
194
  - spec/support/helpers/factories.rb
193
195
  - spec/support/shared_examples/a_class_that_extends_active_interactor_models_example.rb
@@ -201,10 +203,10 @@ licenses:
201
203
  - MIT
202
204
  metadata:
203
205
  bug_tracker_uri: https://github.com/aaronmallen/activeinteractor/issues
204
- changelog_uri: https://github.com/aaronmallen/activeinteractor/blob/v1.0.5/CHANGELOG.md
205
- documentation_uri: https://www.rubydoc.info/gems/activeinteractor/1.0.5
206
+ changelog_uri: https://github.com/aaronmallen/activeinteractor/blob/v1.1.0/CHANGELOG.md
207
+ documentation_uri: https://www.rubydoc.info/gems/activeinteractor/1.1.0
206
208
  hompage_uri: https://github.com/aaronmallen/activeinteractor
207
- source_code_uri: https://github.com/aaronmallen/activeinteractor/tree/v1.0.5
209
+ source_code_uri: https://github.com/aaronmallen/activeinteractor/tree/v1.1.0
208
210
  wiki_uri: https://github.com/aaronmallen/activeinteractor/wiki
209
211
  post_install_message:
210
212
  rdoc_options: []
@@ -233,7 +235,9 @@ test_files:
233
235
  - spec/support/shared_examples/a_class_with_interactor_callback_methods_example.rb
234
236
  - spec/support/spec_helpers.rb
235
237
  - spec/support/helpers/factories.rb
238
+ - spec/integration/an_organizer_with_options_callbacks_spec.rb
236
239
  - spec/integration/an_interactor_with_validations_on_calling_spec.rb
240
+ - spec/integration/an_organizer_with_failing_nested_organizer_spec.rb
237
241
  - spec/integration/a_basic_interactor_spec.rb
238
242
  - spec/integration/a_basic_organizer_spec.rb
239
243
  - spec/integration/an_organizer_with_conditionally_organized_interactors_spec.rb