activeinteractor 1.0.5 → 1.1.0

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