activeinteractor 1.0.0 → 1.0.5

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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -1
  3. data/README.md +22 -7
  4. data/lib/active_interactor.rb +5 -1
  5. data/lib/active_interactor/config.rb +1 -1
  6. data/lib/active_interactor/context/attributes.rb +64 -15
  7. data/lib/active_interactor/context/base.rb +87 -0
  8. data/lib/active_interactor/context/errors.rb +47 -0
  9. data/lib/active_interactor/context/loader.rb +1 -1
  10. data/lib/active_interactor/context/status.rb +12 -19
  11. data/lib/active_interactor/interactor/context.rb +59 -3
  12. data/lib/active_interactor/interactor/perform.rb +5 -1
  13. data/lib/active_interactor/models.rb +13 -28
  14. data/lib/active_interactor/organizer/interactor_interface.rb +1 -1
  15. data/lib/active_interactor/organizer/interactor_interface_collection.rb +1 -0
  16. data/lib/active_interactor/rails/orm/dynamoid.rb +5 -0
  17. data/lib/active_interactor/rails/orm/mongoid.rb +5 -0
  18. data/lib/active_interactor/version.rb +1 -1
  19. data/lib/rails/generators/templates/organizer.erb +2 -2
  20. data/spec/active_interactor/base_spec.rb +33 -0
  21. data/spec/active_interactor/context/base_spec.rb +101 -12
  22. data/spec/integration/a_basic_interactor_spec.rb +48 -0
  23. data/spec/integration/a_basic_organizer_spec.rb +119 -0
  24. data/spec/integration/active_record_integration_spec.rb +8 -342
  25. data/spec/spec_helper.rb +18 -8
  26. data/spec/support/shared_examples/a_class_that_extends_active_interactor_models_example.rb +81 -0
  27. metadata +37 -42
  28. data/spec/support/coverage.rb +0 -4
  29. data/spec/support/coverage/reporters.rb +0 -11
  30. data/spec/support/coverage/reporters/codacy.rb +0 -39
  31. data/spec/support/coverage/reporters/simple_cov.rb +0 -54
  32. data/spec/support/coverage/runner.rb +0 -66
@@ -22,7 +22,7 @@ module ActiveInteractor
22
22
  # @param interactor_class [Const] an {ActiveInteractor::Base interactor} class
23
23
  # @return [Const] a class that inherits from {Base}
24
24
  def self.create(context_class_name, interactor_class)
25
- interactor_class.const_set(context_class_name.to_s.classify, Class.new(BASE_CONTEXT))
25
+ interactor_class.const_set(context_class_name.to_s.camelize, Class.new(BASE_CONTEXT))
26
26
  end
27
27
 
28
28
  # Find or create a {Base context} class for a given {ActiveInteractor::Base interactor}. If a class exists
@@ -37,8 +37,9 @@ module ActiveInteractor
37
37
  # @see https://api.rubyonrails.org/classes/ActiveModel/Errors.html ActiveModel::Errors
38
38
  # @raise [Error::ContextFailure]
39
39
  def fail!(errors = nil)
40
- merge_errors!(errors) if errors
40
+ handle_errors(errors) if errors
41
41
  @_failed = true
42
+ resolve
42
43
  raise ActiveInteractor::Error::ContextFailure, self
43
44
  end
44
45
 
@@ -59,6 +60,16 @@ module ActiveInteractor
59
60
  end
60
61
  alias fail? failure?
61
62
 
63
+ # Resolve an instance of {Base context}. Called when an interactor
64
+ # is finished with it's context.
65
+ #
66
+ # @since 1.0.3
67
+ # @return [self] the instance of {Base context}
68
+ def resolve
69
+ resolve_errors
70
+ self
71
+ end
72
+
62
73
  # {#rollback! Rollback} an instance of {Base context}. Any {ActiveInteractor::Base interactors} the instance has
63
74
  # been passed via the {#called!} method are asked to roll themselves back by invoking their
64
75
  # {Interactor::Perform#rollback #rollback} methods. The instance is also flagged as rolled back.
@@ -90,24 +101,6 @@ module ActiveInteractor
90
101
  !failure?
91
102
  end
92
103
  alias successful? success?
93
-
94
- private
95
-
96
- def _called
97
- @_called ||= []
98
- end
99
-
100
- def copy_called!(context)
101
- value = context.instance_variable_get('@_called') || []
102
- instance_variable_set('@_called', value)
103
- end
104
-
105
- def copy_flags!(context)
106
- %w[_failed _rolled_back].each do |flag|
107
- value = context.instance_variable_get("@#{flag}")
108
- instance_variable_set("@#{flag}", value)
109
- end
110
- end
111
104
  end
112
105
  end
113
106
  end
@@ -27,7 +27,43 @@ module ActiveInteractor
27
27
  #
28
28
  # MyInteractor.context_class.attributes
29
29
  # #=> [:first_name, :last_name]
30
- delegate :attributes, to: :context_class, prefix: :context
30
+ #
31
+ # @!method context_attribute_missing(match, *args, &block)
32
+ # Call {ActiveInteractor::Context::Base.attribute_missing .attribute_missing} on the {Base interactor} class'
33
+ # {#context_class context class}
34
+ #
35
+ # @since 1.0.1
36
+ #
37
+ # @!method context_respond_to_without_attributes?(method, include_private_methods = false)
38
+ # Call {ActiveInteractor::Context::Base.respond_to_without_attributes? .respond_to_without_attributes?} on the
39
+ # {Base interactor} class' {#context_class context class}
40
+ #
41
+ # @since 1.0.1
42
+ delegate :attributes, :attribute_missing, :respond_to_without_attributes?, to: :context_class, prefix: :context
43
+
44
+ # @!method context_attribute(name, type=Type::Value.new, **options)
45
+ # Call {ActiveInteractor::Context::Base.attribute .attribute} on the {Base interactor} class'
46
+ # {#context_class context class}
47
+ #
48
+ # @since 1.0.1
49
+ #
50
+ # @example Setting default values on the {ActiveInteractor::Context::Base context} class
51
+ # class MyInteractor < ActiveInteractor::Base
52
+ # context_attribute :first_name, default: -> { 'Aaron' }
53
+ # end
54
+ #
55
+ # MyInteractor.perform
56
+ # #=> <#MyInteractor::Context first_name='Aaron'>
57
+ #
58
+ # MyInteractor.perform(first_name: 'Bob')
59
+ # #=> <#MyInteractor::Context first_name='Bob'>
60
+ #
61
+ # @!method context_attribute_names
62
+ # Call {ActiveInteractor::Context::Base.attribute_names .attribute_names} on the {Base interactor} class'
63
+ # {#context_class context class}
64
+ #
65
+ # @since 1.0.1
66
+ delegate(*ActiveModel::Attributes::ClassMethods.instance_methods, to: :context_class, prefix: :context)
31
67
 
32
68
  # @!method context_attribute_method?(attribute)
33
69
  # Call {ActiveInteractor::Context::Base.attribute_method? .attribute_method?} on the {Base interactor} class'
@@ -175,7 +211,7 @@ module ActiveInteractor
175
211
  # @return [Const] the {Base interactor} class' {ActiveInteractor::Context::Base context} class
176
212
  def contextualize_with(klass)
177
213
  @context_class = begin
178
- context_class = klass.to_s.classify.safe_constantize
214
+ context_class = klass.to_s.camelize.safe_constantize
179
215
  raise(ActiveInteractor::Error::InvalidContextClass, klass) unless context_class
180
216
 
181
217
  context_class
@@ -183,18 +219,37 @@ module ActiveInteractor
183
219
  end
184
220
  end
185
221
 
222
+ # @!method context_attribute_missing(match, *args, &block)
223
+ # Call {ActiveInteractor::Context::Base#attribute_missing #attribute_missing} on the {Base interactor} instance's
224
+ # {ActiveInteractor::Context::Base context} instance
225
+ #
226
+ # @since 1.0.1
227
+ #
228
+ # @!method context_attribute_names
229
+ # Call {ActiveInteractor::Context::Base#attribute_names #attribute_names} on the {Base interactor} instance's
230
+ # {ActiveInteractor::Context::Base context} instance
231
+ #
232
+ # @since 1.0.1
233
+ #
186
234
  # @!method context_fail!(errors = nil)
187
235
  # Call {ActiveInteractor::Context::Status#fail! #fail!} on the {Base interactor} instance's
188
236
  # {ActiveInteractor::Context::Base context} instance
189
237
  #
190
238
  # @since 1.0.0
191
239
  #
240
+ # @!method context_respond_to_without_attributes?(method, include_private_methods = false)
241
+ # Call {ActiveInteractor::Context::Base#respond_to_without_attributes? #respond_to_without_attributes?} on the
242
+ # {Base interactor} instance's {ActiveInteractor::Context::Base context} instance
243
+ #
244
+ # @since 1.0.1
245
+ #
192
246
  # @!method context_rollback!
193
247
  # Call {ActiveInteractor::Context::Status#rollback! #rollback!} on the {Base interactor} instance's
194
248
  # {ActiveInteractor::Context::Base context} instance
195
249
  #
196
250
  # @since 1.0.0
197
- delegate :fail!, :rollback!, to: :context, prefix: true
251
+ delegate :attribute_missing, :attribute_names, :fail!, :respond_to_without_attributes?, :rollback!,
252
+ to: :context, prefix: true
198
253
 
199
254
  # @!method context_errors
200
255
  # Call {ActiveInteractor::Context::Base#errors #errors} on the {Base interactor} instance's
@@ -311,6 +366,7 @@ module ActiveInteractor
311
366
  # @return [Class] the {ActiveInteractor::Context::Base context} instance
312
367
  def finalize_context!
313
368
  context.called!(self)
369
+ context.resolve
314
370
  context
315
371
  end
316
372
 
@@ -175,7 +175,11 @@ module ActiveInteractor
175
175
  #
176
176
  # @return [Base] a duplicated {Base interactor} instance
177
177
  def deep_dup
178
- self.class.new(context.dup).with_options(options.dup)
178
+ dupped = dup
179
+ %w[@context @options].each do |variable|
180
+ dupped.instance_variable_set(variable, instance_variable_get(variable)&.dup)
181
+ end
182
+ dupped
179
183
  end
180
184
 
181
185
  # Options for the {Base interactor} {#perform}
@@ -13,39 +13,21 @@ module ActiveInteractor
13
13
  # @author Aaron Allen <hello@aaronmallen.me>
14
14
  # @since 1.0.0
15
15
  module InstanceMethods
16
- def initialize(attributes = nil)
17
- copy_flags!(attributes) if attributes
18
- copy_called!(attributes) if attributes
19
- attributes_as_hash = context_attributes_as_hash(attributes)
20
- super(attributes_as_hash)
21
- end
22
-
23
- # Merge an instance of model class into the calling model class instance
24
- #
25
- # @see Context::Attributes#merge!
26
- #
27
- # @param context [Class] a {Base context} instance to be merged
28
- # @return [self] the {Base context} instance
29
- def merge!(context)
30
- copy_flags!(context)
31
- context.each_pair do |key, value|
32
- self[key] = value unless value.nil?
33
- end
34
- self
35
- end
36
-
37
- private
38
-
39
- def context_attributes_as_hash(attributes)
40
- return attributes.to_h if attributes&.respond_to?(:to_h)
41
- return attributes.attributes.to_h if attributes.respond_to?(:attributes)
16
+ def initialize(*)
17
+ @attributes = self.class._default_attributes&.deep_dup
18
+ super
42
19
  end
43
20
  end
44
21
 
45
22
  # Include methods needed for a {Context::Base context} class to function properly.
46
23
  #
24
+ # @note You must include ActiveModel::Model and ActiveModel::Attributes or a similar implementation for
25
+ # the object to function properly.
26
+ #
47
27
  # @example
48
28
  # class User
29
+ # include ActiveModel::Model
30
+ # include ActiveModel::Attributes
49
31
  # extend ActiveInteractor::Models
50
32
  # acts_as_context
51
33
  # end
@@ -55,9 +37,12 @@ module ActiveInteractor
55
37
  # end
56
38
  def acts_as_context
57
39
  class_eval do
58
- include ActiveModel::Validations
59
- include ActiveInteractor::Models::InstanceMethods
40
+ extend ActiveInteractor::Context::Attributes::ClassMethods
41
+
42
+ include ActiveInteractor::Context::Attributes
43
+ include ActiveInteractor::Context::Errors
60
44
  include ActiveInteractor::Context::Status
45
+ include ActiveInteractor::Models::InstanceMethods
61
46
  delegate :each_pair, to: :attributes
62
47
  end
63
48
  end
@@ -40,7 +40,7 @@ module ActiveInteractor
40
40
  # {Interactor::Perform::ClassMethods#perform .perform}. See {Interactor::Perform::Options}.
41
41
  # @return [InteractorInterface] a new instance of {InteractorInterface}
42
42
  def initialize(interactor_class, options = {})
43
- @interactor_class = interactor_class.to_s.classify.safe_constantize
43
+ @interactor_class = interactor_class.to_s.camelize.safe_constantize
44
44
  @filters = options.select { |key, _value| CONDITIONAL_FILTERS.include?(key) }
45
45
  @perform_options = options.reject { |key, _value| CONDITIONAL_FILTERS.include?(key) }
46
46
  end
@@ -14,6 +14,7 @@ module ActiveInteractor
14
14
  # @return [Array<InteractorInterface>] the {InteractorInterface} collection
15
15
  class InteractorInterfaceCollection
16
16
  attr_reader :collection
17
+
17
18
  # @!method map(&block)
18
19
  # Invokes the given block once for each element of {#collection}.
19
20
  # @return [Array] a new array containing the values returned by the block.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dynamoid/document'
4
+
5
+ Dynamoid::Document::ClassMethods.include ActiveInteractor::Models
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveSupport.on_load(:mongoid) do
4
+ Mongoid::Document::ClassMethods.include ActiveInteractor::Models
5
+ end
@@ -3,5 +3,5 @@
3
3
  module ActiveInteractor
4
4
  # The ActiveInteractor version
5
5
  # @return [String] the ActiveInteractor version
6
- VERSION = '1.0.0'
6
+ VERSION = '1.0.5'
7
7
  end
@@ -6,8 +6,8 @@ class <%= class_name %> < ApplicationOrganizer
6
6
 
7
7
  <%- end -%>
8
8
  <%- if interactors.any? -%>
9
- organize <%= interactors.map(&:classify).join(", ") %>
9
+ organize <%= interactors.join(", ") %>
10
10
  <%- else -%>
11
- # organize Interactor1, Interactor2
11
+ # organize :interactor_1, :interactor_2
12
12
  <%- end -%>
13
13
  end
@@ -27,6 +27,17 @@ RSpec.describe ActiveInteractor::Base do
27
27
  subject
28
28
  expect(described_class.context_class).to eq TestContext
29
29
  end
30
+
31
+ # https://github.com/aaronmallen/activeinteractor/issues/168
32
+ context 'when singularized' do
33
+ let!(:singularized_class) { build_context('PlaceData') }
34
+ let(:klass) { 'PlaceData' }
35
+
36
+ it 'is expected to assign the appropriate context class' do
37
+ subject
38
+ expect(described_class.context_class).to eq PlaceData
39
+ end
40
+ end
30
41
  end
31
42
 
32
43
  context 'when passed as a symbol' do
@@ -36,6 +47,17 @@ RSpec.describe ActiveInteractor::Base do
36
47
  subject
37
48
  expect(described_class.context_class).to eq TestContext
38
49
  end
50
+
51
+ # https://github.com/aaronmallen/activeinteractor/issues/168
52
+ context 'when singularized' do
53
+ let!(:singularized_class) { build_context('PlaceData') }
54
+ let(:klass) { :place_data }
55
+
56
+ it 'is expected to assign the appropriate context class' do
57
+ subject
58
+ expect(described_class.context_class).to eq PlaceData
59
+ end
60
+ end
39
61
  end
40
62
 
41
63
  context 'when passed as a constant' do
@@ -45,6 +67,17 @@ RSpec.describe ActiveInteractor::Base do
45
67
  subject
46
68
  expect(described_class.context_class).to eq TestContext
47
69
  end
70
+
71
+ # https://github.com/aaronmallen/activeinteractor/issues/168
72
+ context 'when singularized' do
73
+ let!(:singularized_class) { build_context('PlaceData') }
74
+ let(:klass) { PlaceData }
75
+
76
+ it 'is expected to assign the appropriate context class' do
77
+ subject
78
+ expect(described_class.context_class).to eq PlaceData
79
+ end
80
+ end
48
81
  end
49
82
  end
50
83
  end
@@ -3,34 +3,113 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  RSpec.describe ActiveInteractor::Context::Base do
6
- after(:each) { described_class.instance_variable_set('@__attributes', []) }
7
-
8
- describe '#attributes' do
6
+ describe '.attributes' do
9
7
  context 'when no arguments are passed' do
10
- subject { described_class.attributes }
8
+ subject { context_class.attributes }
9
+ let!(:context_class) { build_context }
11
10
  it { is_expected.to eq [] }
12
11
 
13
12
  context 'when an attribute :foo was previously defined' do
14
- before { described_class.instance_variable_set('@__attributes', %i[foo]) }
13
+ let!(:context_class) do
14
+ build_context do
15
+ attributes :foo
16
+ end
17
+ end
15
18
 
16
19
  it { is_expected.to eq %i[foo] }
17
20
  end
18
21
  end
19
22
 
20
23
  context 'when given arguments :foo and :bar' do
21
- subject { described_class.attributes(:foo, :bar) }
24
+ subject { context_class.attributes(:foo, :bar) }
25
+ let!(:context_class) { build_context }
22
26
 
23
27
  it { is_expected.to eq %i[bar foo] }
24
28
 
25
29
  context 'when an attribute :foo was previously defined' do
26
- before { described_class.instance_variable_set('@__attributes', %i[foo]) }
30
+ before { TestContext.attributes(:foo) }
27
31
 
28
32
  it { is_expected.to eq %i[bar foo] }
29
33
  end
30
34
  end
31
35
  end
32
36
 
33
- describe '.attributes' do
37
+ describe '#[]' do
38
+ subject { instance[attribute] }
39
+
40
+ context 'with class attributes []' do
41
+ let(:instance) { build_context.new }
42
+
43
+ context 'with attribute nil' do
44
+ let(:attribute) { :foo }
45
+
46
+ it { is_expected.to be_nil }
47
+ end
48
+
49
+ context 'with attribute equal to "foo"' do
50
+ let(:attribute) { :foo }
51
+ before { instance.foo = 'foo' }
52
+
53
+ it { is_expected.to eq 'foo' }
54
+ end
55
+ end
56
+
57
+ context 'with class attributes [:foo]' do
58
+ let!(:context_class) do
59
+ build_context do
60
+ attributes :foo
61
+ end
62
+ end
63
+ let(:instance) { context_class.new }
64
+
65
+ context 'with attribute nil' do
66
+ let(:attribute) { :foo }
67
+
68
+ it { is_expected.to be_nil }
69
+ end
70
+
71
+ context 'with attribute equal to "foo"' do
72
+ let(:attribute) { :foo }
73
+ before { instance.foo = 'foo' }
74
+
75
+ it { is_expected.to eq 'foo' }
76
+ end
77
+ end
78
+ end
79
+
80
+ describe '#attribute?' do
81
+ subject { instance.attribute?(attribute) }
82
+
83
+ context 'with class attributes []' do
84
+ let(:instance) { build_context.new }
85
+ let(:attribute) { :foo }
86
+
87
+ it { is_expected.to eq false }
88
+ end
89
+
90
+ context 'with class attributes [:foo]' do
91
+ let!(:context_class) do
92
+ build_context do
93
+ attributes :foo
94
+ end
95
+ end
96
+ let(:instance) { context_class.new }
97
+
98
+ context 'checking attribute :foo' do
99
+ let(:attribute) { :foo }
100
+
101
+ it { is_expected.to eq true }
102
+ end
103
+
104
+ context 'checking attribute :bar' do
105
+ let(:attribute) { :bar }
106
+
107
+ it { is_expected.to eq false }
108
+ end
109
+ end
110
+ end
111
+
112
+ describe '#attributes' do
34
113
  subject { instance.attributes }
35
114
 
36
115
  context 'with class attributes []' do
@@ -43,13 +122,23 @@ RSpec.describe ActiveInteractor::Context::Base do
43
122
  end
44
123
 
45
124
  context 'with class attributes [:foo, :bar, :baz]' do
46
- before { described_class.attributes(:foo, :bar, :baz) }
125
+ before { build_context.attributes(:foo, :bar) }
126
+
127
+ context 'with an instance having attributes { :foo => "foo", :bar => "bar" }' do
128
+ let(:instance) { TestContext.new(foo: 'foo', bar: 'bar') }
129
+
130
+ it { is_expected.to be_a Hash }
131
+ it { is_expected.to eq(bar: 'bar', foo: 'foo') }
132
+ end
47
133
 
48
134
  context 'with an instance having attributes { :foo => "foo", :bar => "bar", :baz => "baz" }' do
49
- let(:instance) { described_class.new(foo: 'foo', bar: 'bar', baz: 'baz') }
135
+ let(:instance) { TestContext.new(foo: 'foo', bar: 'bar', baz: 'baz') }
50
136
 
51
137
  it { is_expected.to be_a Hash }
52
- it { is_expected.to eq(bar: 'bar', baz: 'baz', foo: 'foo') }
138
+ it { is_expected.to eq(bar: 'bar', foo: 'foo') }
139
+ it 'is expected to assign :baz' do
140
+ expect(instance.baz).to eq 'baz'
141
+ end
53
142
  end
54
143
  end
55
144
  end
@@ -81,7 +170,7 @@ RSpec.describe ActiveInteractor::Context::Base do
81
170
  it { expect { subject }.to raise_error(ActiveInteractor::Error::ContextFailure) }
82
171
 
83
172
  it 'is expected not to merge errors' do
84
- expect(instance.errors).not_to receive(:merge!)
173
+ expect(instance.errors).not_to receive(:merge!).with(nil)
85
174
  subject
86
175
  rescue ActiveInteractor::Error::ContextFailure # rubocop:disable Lint/SuppressedException
87
176
  end