activeinteractor 1.0.0.beta.2 → 1.0.0.beta.3

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: 1dcf3dc4d0f528400e6322827f343a2e4652fd71611061af586c0f984acc9e5e
4
- data.tar.gz: b295be6c1061186c44094cc9aececec4be23719a3033dfd25a8dfb784a732e72
3
+ metadata.gz: ec758d003a313ede93fb1abbabc42cae23e071b9c675a9515006a5fb0e3a02a4
4
+ data.tar.gz: 6e1c83a0e0f7a37f63a9d9523cfbad34ccd8fb1996c5329e91294c772f4898bc
5
5
  SHA512:
6
- metadata.gz: 5910c5d2c343125cd715894d646975c356737d66a62390240d0189e241fe98766099a05231ea7d7b382aa8cfdaa2ab44a4b12c3d3a178355130991bf47ec5ebd
7
- data.tar.gz: 8892204ed0d17a68b595bda665947fc89ad67a6e1cd5d29ed303c18312a5fb3719252bf14684254ea68c20500daae38afce46e102d48c2e8b2f0793bb6005729
6
+ metadata.gz: 91ccd10ef1bd421dcf3275a980fe279de279c60a52b4815c0f4ae8b44c6d4721ed135f2e3978dd1c6cca4bc1acc3fd064013023ac89e48a1fd4fb53ba4c32f7d
7
+ data.tar.gz: fec0e7ab4a5976efae4b0c64cc4593789b914d8c6208966575af9f79b6ebbd495d30c2c9e5ef942a060334620b6dfca946c67c27a694a36295bf194d41cd4887
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [v1.0.0-beta.3] - 2020-01-12
11
+
12
+ ### Added
13
+
14
+ - [#109] `ActiveInteractor::Organizer.parallel`
15
+ - [#109] `ActiveInteractor::Organizer.perform_in_parallel`
16
+ - [#109] `ActiveInteractor::Context::Base#merge`
17
+ - [#110] `ActiveInteractor::Interactor::PerformOptions`
18
+
19
+ ### Changed
20
+
21
+ - [#110] `ActiveInteractor::Interactor.perform` now takes options
22
+
10
23
  ## [v1.0.0-beta.2] - 2020-01-07
11
24
 
12
25
  ### Added
@@ -134,7 +147,8 @@ and this project adheres to [Semantic Versioning].
134
147
 
135
148
  <!-- versions -->
136
149
 
137
- [Unreleased]: https://github.com/aaronmallen/activeinteractor/compare/v1.0.0-beta.2...HEAD
150
+ [Unreleased]: https://github.com/aaronmallen/activeinteractor/compare/v1.0.0-beta.3...HEAD
151
+ [v1.0.0-beta.3]: https://github.com/aaronmallen/activeinteractor/compare/v1.0.0-beta.2...v1.0.0-beta.3
138
152
  [v1.0.0-beta.2]: https://github.com/aaronmallen/activeinteractor/compare/v1.0.0-beta.1...v1.0.0-beta.2
139
153
  [v1.0.0-beta.1]: https://github.com/aaronmallen/activeinteractor/compare/v0.1.7...v1.0.0-beta.1
140
154
  [v0.1.7]: https://github.com/aaronmallen/activeinteractor/compare/v0.1.6...v0.1.7
@@ -165,3 +179,5 @@ and this project adheres to [Semantic Versioning].
165
179
  [#103]: https://github.com/aaronmallen/activeinteractor/pull/103
166
180
  [#104]: https://github.com/aaronmallen/activeinteractor/pull/104
167
181
  [#105]: https://github.com/aaronmallen/activeinteractor/pull/105
182
+ [#109]: https://github.com/aaronmallen/activeinteractor/pull/109
183
+ [#110]: https://github.com/aaronmallen/activeinteractor/pull/110
data/README.md CHANGED
@@ -24,8 +24,9 @@ Ruby interactors with [ActiveModel::Validations] based on the [interactor][colle
24
24
  * [Validating the Context](#validating-the-context)
25
25
  * [Using Interactors](#using-interactors)
26
26
  * [Kinds of Interactors](#kinds-of-interactors)
27
- * [Interactors](#interactors)
28
- * [Organizers](#organizers)
27
+ * [Interactors](#interactors)
28
+ * [Organizers](#organizers)
29
+ * [Parallel Organizers](#parallel-organizers)
29
30
  * [Rollback](#rollback)
30
31
  * [Callbacks](#callbacks)
31
32
  * [Validation Callbacks](#validation-callbacks)
@@ -346,7 +347,7 @@ Finally, the context (along with any changes made to it) is returned.
346
347
 
347
348
  There are two kinds of interactors built into the Interactor library: basic interactors and organizers.
348
349
 
349
- #### Interactors
350
+ ##### Interactors
350
351
 
351
352
  A basic interactor is a class that includes Interactor and defines `perform`.\
352
353
 
@@ -366,7 +367,7 @@ end
366
367
 
367
368
  Basic interactors are the building blocks. They are your application's single-purpose units of work.
368
369
 
369
- #### Organizers
370
+ ##### Organizers
370
371
 
371
372
  An organizer is an important variation on the basic interactor. Its single purpose is to run other interactors.
372
373
 
@@ -421,6 +422,41 @@ end
421
422
  The organizer passes its context to the interactors that it organizes, one at a time and in order. Each interactor may
422
423
  change that context before it's passed along to the next interactor.
423
424
 
425
+ ##### Parallel Organizers
426
+
427
+ Organizers can be told to run their interactors in parallel with the `#perform_in_parallel` class method. This
428
+ will run each interactor in parallel with one and other only passing the original context to each organizer.
429
+ This means each interactor must be able to perform without dependencies on prior interactor runs.
430
+
431
+ ```ruby
432
+ class CreateNewUser < ActiveInteractor::Base
433
+ def perform
434
+ context.user = User.create(
435
+ first_name: context.first_name,
436
+ last_name: context.last_name
437
+ )
438
+ end
439
+ end
440
+
441
+ class LogNewUserCreation < ActiveInteractor::Base
442
+ def perform
443
+ context.log = Log.create(
444
+ event: 'new user created',
445
+ first_name: context.first_name,
446
+ last_name: context.last_name
447
+ )
448
+ end
449
+ end
450
+
451
+ class CreateUser < ActiveInteractor::Organizer
452
+ perform_in_parallel
453
+ organize :create_new_user, :log_new_user_creation
454
+ end
455
+
456
+ CreateUser.perform(first_name: 'Aaron', last_name: 'Allen')
457
+ #=> <#CreateUser::Context first_name='Aaron' last_name='Allen' user=>#<User ...> log=<#Log ...>>
458
+ ```
459
+
424
460
  #### Rollback
425
461
 
426
462
  If any one of the organized interactors fails its context, the organizer stops. If the `ChargeCard` interactor fails,
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'logger'
4
4
 
5
+ require 'active_interactor/configurable'
6
+
5
7
  module ActiveInteractor
6
8
  # The ActiveInteractor configuration object
7
9
  # @author Aaron Allen <hello@aaronmallen.me>
@@ -9,21 +11,13 @@ module ActiveInteractor
9
11
  # @!attribute [rw] logger
10
12
  # @return [Logger] an instance of Logger
11
13
  class Config
12
- # @return [Hash{Symbol=>*}] the default configuration options
13
- DEFAULTS = {
14
- logger: Logger.new(STDOUT)
15
- }.freeze
16
-
17
- attr_accessor :logger
14
+ include Configurable
15
+ defaults logger: Logger.new(STDOUT)
18
16
 
17
+ # @!method initialize(options = {})
19
18
  # @param options [Hash] the options for the configuration
20
19
  # @option options [Logger] :logger the configuration logger instance
21
20
  # @return [Config] a new instance of {Config}
22
- def initialize(options = {})
23
- DEFAULTS.dup.merge(options).each do |key, value|
24
- instance_variable_set("@#{key}", value)
25
- end
26
- end
27
21
 
28
22
  # The rails configuration object
29
23
  # @return [Rails::Config|nil] an instance of {Rails::Config} if `Rails` is
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/class/attribute'
4
+
5
+ module ActiveInteractor
6
+ # Methods for configurable objects
7
+ # @author Aaron Allen <hello@aaronmallen.me>
8
+ # @since 1.0.0
9
+ module Configurable
10
+ def self.included(base)
11
+ base.class_eval do
12
+ extend ClassMethods
13
+ end
14
+ end
15
+
16
+ # Class methods for configuable objects
17
+ module ClassMethods
18
+ # Set or get default options for a configurable object
19
+ # @param options [Hash] the options to set for defaults
20
+ # @return [Hash] the defaults
21
+ def defaults(options = {})
22
+ return __defaults if options.empty?
23
+
24
+ options.each do |key, value|
25
+ __defaults[key.to_sym] = value
26
+ send(:attr_accessor, key.to_sym)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def __defaults
33
+ @__defaults ||= {}
34
+ end
35
+ end
36
+
37
+ # @param options [Hash] the options for the configuration
38
+ # @return [Configurable] a new instance of {Configurable}
39
+ def initialize(options = {})
40
+ self.class.defaults.merge(options).each do |key, value|
41
+ instance_variable_set("@#{key}", value)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -36,14 +36,6 @@ module ActiveInteractor
36
36
  end
37
37
  end
38
38
 
39
- # @api private
40
- # @param context [Hash|Context::Base] attributes to assign to the context
41
- # @return [Context::Base] a new instance of {Context::Base}
42
- def initialize(context = {})
43
- copy_flags!(context)
44
- super
45
- end
46
-
47
39
  # Attributes defined on the instance
48
40
  # @example Get attributes defined on an instance
49
41
  # class MyInteractor::Context < ActiveInteractor::Context::Base
@@ -65,15 +57,6 @@ module ActiveInteractor
65
57
  hash[attribute] = self[attribute] if self[attribute]
66
58
  end
67
59
  end
68
-
69
- private
70
-
71
- def copy_flags!(context)
72
- %w[_called _failed _rolled_back].each do |flag|
73
- value = context.instance_variable_get("@#{flag}")
74
- instance_variable_set("@#{flag}", value)
75
- end
76
- end
77
60
  end
78
61
  end
79
62
  end
@@ -15,6 +15,15 @@ module ActiveInteractor
15
15
  include ActiveModel::Validations
16
16
  include Attributes
17
17
 
18
+ # @param context [Hash|Context::Base] attributes to assign to the context
19
+ # @return [Context::Base] a new instance of {Context::Base}
20
+ def initialize(context = {})
21
+ merge_errors!(context.errors) if context.respond_to?(:errors)
22
+ copy_flags!(context)
23
+ copy_called!(context)
24
+ super
25
+ end
26
+
18
27
  # @!method valid?(context = nil)
19
28
  # @see
20
29
  # https://github.com/rails/rails/blob/master/activemodel/lib/active_model/validations.rb#L305
@@ -72,6 +81,37 @@ module ActiveInteractor
72
81
  end
73
82
  alias fail? failure?
74
83
 
84
+ # Merge an instance of context or a hash into an existing context
85
+ # @since 1.0.0
86
+ # @example
87
+ # class MyInteractor1 < ActiveInteractor::Base
88
+ # def perform
89
+ # context.first_name = 'Aaron'
90
+ # end
91
+ # end
92
+ #
93
+ # class MyInteractor2 < ActiveInteractor::Base
94
+ # def perform
95
+ # context.last_name = 'Allen'
96
+ # end
97
+ # end
98
+ #
99
+ # result = MyInteractor1.perform
100
+ # #=> <#MyInteractor1::Context first_name='Aaron'>
101
+ #
102
+ # result.merge!(MyInteractor2.perform)
103
+ # #=> <#MyInteractor1::Context first_name='Aaron' last_name='Allen'>
104
+ # @param context [Base|Hash] attributes to merge into the context
105
+ # @return [Base] an instance of {Base}
106
+ def merge!(context)
107
+ merge_errors!(context.errors) if context.respond_to?(:errors)
108
+ copy_flags!(context)
109
+ context.each_pair do |key, value|
110
+ self[key] = value
111
+ end
112
+ self
113
+ end
114
+
75
115
  # Roll back an interactor context. Any interactors to which this
76
116
  # context has been passed and which have been successfully called are asked
77
117
  # to roll themselves back by invoking their
@@ -129,6 +169,18 @@ module ActiveInteractor
129
169
  @_called ||= []
130
170
  end
131
171
 
172
+ def copy_called!(context)
173
+ value = context.instance_variable_get('@_called') || []
174
+ instance_variable_set('@_called', value)
175
+ end
176
+
177
+ def copy_flags!(context)
178
+ %w[_failed _rolled_back].each do |flag|
179
+ value = context.instance_variable_get("@#{flag}")
180
+ instance_variable_set("@#{flag}", value)
181
+ end
182
+ end
183
+
132
184
  def merge_errors!(errors)
133
185
  if errors.is_a? String
134
186
  self.errors.add(:context, errors)
@@ -14,6 +14,7 @@ module ActiveInteractor
14
14
  delegate :execute_perform, :execute_perform!, :execute_rollback, to: :worker
15
15
  end
16
16
  end
17
+
17
18
  # Interactor class methods.
18
19
  module ClassMethods
19
20
  # Run an interactor context. This it the primary API method for
@@ -21,11 +22,11 @@ module ActiveInteractor
21
22
  # @example Run an interactor
22
23
  # MyInteractor.perform(name: 'Aaron')
23
24
  # #=> <#MyInteractor::Context name='Aaron'>
24
- # @param context [Hash|Context::Base] properties to assign to
25
- # an interactor's context.
25
+ # @param context [Hash|Context::Base] attributes to assign to the interactor context
26
+ # @param options [PerformOptions|Hash] execution options for the interactor perform step
26
27
  # @return [Context::Base] an instance of context.
27
- def perform(context = {})
28
- new(context).execute_perform
28
+ def perform(context = {}, options = PerformOptions.new)
29
+ new(context).execute_perform(options)
29
30
  end
30
31
 
31
32
  # Run an interactor context. The {.perform!} method behaves identically to
@@ -35,11 +36,12 @@ module ActiveInteractor
35
36
  # @example Run an interactor
36
37
  # MyInteractor.perform!(name: 'Aaron')
37
38
  # #=> <#MyInteractor::Context name='Aaron'>
38
- # @param context [Hash|Context::Base] properties to assign to the interactor context.
39
+ # @param context [Hash|Context::Base] attributes to assign to the interactor context
40
+ # @param options [PerformOptions|Hash] execution options for the interactor perform step
39
41
  # @raise [Error::ContextFailure] if the context fails.
40
42
  # @return [Context::Base] an instance of context.
41
- def perform!(context = {})
42
- new(context).execute_perform!
43
+ def perform!(context = {}, options = PerformOptions.new)
44
+ new(context).execute_perform!(options)
43
45
  end
44
46
  end
45
47
 
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module Interactor
5
+ # Options object for interactor perform
6
+ # @author Aaron Allen <hello@aaronmallen.me>
7
+ # @since 1.0.0
8
+ # @!attribute [rw] skip_perform_callbacks
9
+ # @return [Boolean] whether or not to skip :perform callbacks
10
+ # @!attribute [rw] skip_rollback
11
+ # @return [Boolean] whether or not to skip rollback when an interactor
12
+ # fails its context
13
+ # @!attribute [rw] skip_rollback_callbacks
14
+ # @return [Boolean] whether or not to skip :rollback callbacks
15
+ # @!attribute [rw] validate
16
+ # @return [Boolean] whether or not to run validations on an interactor
17
+ # @!attribute [rw] validate_on_calling
18
+ # @return [Boolean] whether or not to run validation on :calling
19
+ # @!attribute [rw] validate_on_called
20
+ # @return [Boolean] whether or not to run validation on :called
21
+ class PerformOptions
22
+ # Default options for interactor perform
23
+ # @return [Hash{Symbol=>*}] the default options
24
+ DEFAULTS = {
25
+ skip_perform_callbacks: false,
26
+ skip_rollback: false,
27
+ skip_rollback_callbacks: false,
28
+ validate: true,
29
+ validate_on_calling: true,
30
+ validate_on_called: true
31
+ }.freeze
32
+
33
+ attr_accessor :skip_perform_callbacks, :skip_rollback, :skip_rollback_callbacks,
34
+ :validate, :validate_on_calling, :validate_on_called
35
+
36
+ # @param options [Hash] the attributes for the {PerformOptions}
37
+ # @return [PerformOptions] a new instance of {PerformOptions}
38
+ def initialize(options = {})
39
+ DEFAULTS.dup.merge(options).each do |key, value|
40
+ instance_variable_set("@#{key}", value)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -17,45 +17,83 @@ module ActiveInteractor
17
17
  end
18
18
 
19
19
  # Calls {#execute_perform!} and rescues {Error::ContextFailure}
20
+ # @param options [PerformOptions|Hash] execution options for the interactor perform step
20
21
  # @return [Context::Base] an instance of {Context::Base}
21
- def execute_perform
22
- execute_perform!
22
+ def execute_perform(options = PerformOptions.new)
23
+ execute_perform!(options)
23
24
  rescue Error::ContextFailure => e
24
25
  ActiveInteractor.logger.error("ActiveInteractor: #{e}")
25
26
  context
26
27
  end
27
28
 
28
29
  # Calls {Interactor#perform} with callbacks and context validation
30
+ # @param options [PerformOptions|Hash] execution options for the interactor perform step
29
31
  # @raise [Error::ContextFailure] if the context fails
30
32
  # @return [Context::Base] an instance of {Context::Base}
31
- def execute_perform!
32
- run_callbacks :perform do
33
- execute_context!
34
- @context = interactor.finalize_context!
35
- rescue StandardError
36
- @context = interactor.finalize_context!
37
- execute_rollback
38
- raise
39
- end
33
+ def execute_perform!(options = PerformOptions.new)
34
+ options = parse_options(options)
35
+ execute_context!(options)
36
+ rescue StandardError => e
37
+ handle_error(e, options)
40
38
  end
41
39
 
42
40
  # Calls {Interactor#rollback} with callbacks
43
41
  # @return [Boolean] `true` if rolled back successfully or `false` if already
44
42
  # rolled back
45
- def execute_rollback
46
- run_callbacks :rollback do
47
- interactor.context_rollback!
48
- end
43
+ def execute_rollback(options = PerformOptions.new)
44
+ options = parse_options(options)
45
+ return if options.skip_rollback
46
+
47
+ execute_interactor_rollback!(options)
49
48
  end
50
49
 
51
50
  private
52
51
 
53
52
  attr_reader :context, :interactor
54
53
 
55
- def execute_context!
56
- interactor.context_fail! unless validate_context(:calling)
54
+ def execute_context!(options)
55
+ if options.skip_perform_callbacks
56
+ execute_context_with_validation_check!(options)
57
+ else
58
+ execute_context_with_callbacks!(options)
59
+ end
60
+ end
61
+
62
+ def execute_context_with_callbacks!(options)
63
+ run_callbacks :perform do
64
+ execute_context_with_validation_check!(options)
65
+ @context = interactor.finalize_context!
66
+ end
67
+ end
68
+
69
+ def execute_context_with_validation!(options)
70
+ validate_on_calling(options)
57
71
  interactor.perform
58
- interactor.context_fail! unless validate_context(:called)
72
+ validate_on_called(options)
73
+ end
74
+
75
+ def execute_context_with_validation_check!(options)
76
+ return interactor.perform unless options.validate
77
+
78
+ execute_context_with_validation!(options)
79
+ end
80
+
81
+ def execute_interactor_rollback!(options)
82
+ return interactor.context_rollback! if options.skip_rollback_callbacks
83
+
84
+ run_callbacks :rollback do
85
+ interactor.context_rollback!
86
+ end
87
+ end
88
+
89
+ def handle_error(exception, options)
90
+ @context = interactor.finalize_context!
91
+ execute_rollback(options)
92
+ raise exception
93
+ end
94
+
95
+ def parse_options(options)
96
+ @options = options.is_a?(PerformOptions) ? options : PerformOptions.new(options)
59
97
  end
60
98
 
61
99
  def validate_context(validation_context = nil)
@@ -63,6 +101,18 @@ module ActiveInteractor
63
101
  interactor.context_valid?(validation_context)
64
102
  end
65
103
  end
104
+
105
+ def validate_on_calling(options)
106
+ return unless options.validate_on_calling
107
+
108
+ interactor.context_fail! unless validate_context(:calling)
109
+ end
110
+
111
+ def validate_on_called(options)
112
+ return unless options.validate_on_called
113
+
114
+ interactor.context_fail! unless validate_context(:called)
115
+ end
66
116
  end
67
117
  end
68
118
  end
@@ -11,6 +11,11 @@ module ActiveInteractor
11
11
  # @!attribute [r] organized
12
12
  # @!scope class
13
13
  # @return [Array<Base>] the organized interactors
14
+ # @!attribute [r] parallel
15
+ # @since 1.0.0
16
+ # @!scope class
17
+ # @return [Boolean] whether or not to run the interactors
18
+ # in parallel
14
19
  # @example a basic organizer
15
20
  # class MyInteractor1 < ActiveInteractor::Base
16
21
  # def perform
@@ -32,6 +37,7 @@ module ActiveInteractor
32
37
  # #=> <MyOrganizer::Context interactor1=true interactor2=true>
33
38
  class Organizer < Base
34
39
  class_attribute :organized, instance_writer: false, default: []
40
+ class_attribute :parallel, instance_writer: false, default: false
35
41
  define_callbacks :each_perform
36
42
 
37
43
  # Define a callback to call after each organized interactor's
@@ -183,25 +189,51 @@ module ActiveInteractor
183
189
  end.compact
184
190
  end
185
191
 
192
+ # Run organized interactors in parallel
193
+ # @since 1.0.0
194
+ def self.perform_in_parallel
195
+ self.parallel = true
196
+ end
197
+
186
198
  # Invoke the organized interactors. An organizer is
187
199
  # expected not to define its own {Base#perform} method
188
200
  # in favor of this default implementation.
189
201
  def perform
190
- self.class.organized.each do |interactor|
191
- self.context = execute_interactor_perform_with_callbacks!(interactor)
202
+ if self.class.parallel
203
+ perform_in_parallel
204
+ else
205
+ perform_in_order
192
206
  end
193
- rescue Error::ContextFailure => e
194
- self.context = e.context
195
- ensure
196
- self.context = self.class.context_class.new(context)
197
207
  end
198
208
 
199
209
  private
200
210
 
201
- def execute_interactor_perform_with_callbacks!(interactor)
211
+ def execute_interactor_with_callbacks(interactor, fail_on_error = false, execute_options = {})
202
212
  run_callbacks :each_perform do
203
- interactor.new(context).execute_perform!
213
+ instance = interactor.new(context)
214
+ method = fail_on_error ? :execute_perform! : :execute_perform
215
+ instance.send(method, execute_options)
216
+ end
217
+ end
218
+
219
+ def merge_contexts(contexts)
220
+ contexts.each { |context| @context.merge!(context) }
221
+ context_fail! if contexts.any?(&:failure?)
222
+ end
223
+
224
+ def perform_in_order
225
+ self.class.organized.each do |interactor|
226
+ context.merge!(execute_interactor_with_callbacks(interactor, true))
227
+ end
228
+ rescue Error::ContextFailure => e
229
+ context.merge!(e.context)
230
+ end
231
+
232
+ def perform_in_parallel
233
+ results = self.class.organized.map do |interactor|
234
+ Thread.new { execute_interactor_with_callbacks(interactor, false, skip_rollback: true) }
204
235
  end
236
+ merge_contexts(results.map(&:value))
205
237
  end
206
238
  end
207
239
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_interactor/configurable'
4
+
3
5
  module ActiveInteractor
4
6
  module Rails
5
7
  # The ActiveInteractor rails configuration object
@@ -11,24 +13,15 @@ module ActiveInteractor
11
13
  # @return [Boolean] whether or not to generate a seperate
12
14
  # context class for an interactor.
13
15
  class Config
14
- # @return [Hash{Symbol=>*}] the default configuration options
15
- DEFAULTS = {
16
- directory: 'interactors',
17
- generate_context_classes: true
18
- }.freeze
19
-
20
- attr_accessor :directory, :generate_context_classes
16
+ include ActiveInteractor::Configurable
17
+ defaults directory: 'interactors', generate_context_classes: true
21
18
 
19
+ # @!method initialize(options = {})
22
20
  # @param options [Hash] the options for the configuration
23
21
  # @option options [String] :directory (defaults to: 'interactors') the configuration directory property
24
22
  # @option options [Boolean] :generate_context_classes (defaults to: `true`) the configuration
25
23
  # generate_context_class property.
26
24
  # @return [Config] a new instance of {Config}
27
- def initialize(options = {})
28
- DEFAULTS.dup.merge(options).each do |key, value|
29
- instance_variable_set("@#{key}", value)
30
- end
31
- end
32
25
  end
33
26
  end
34
27
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ActiveInteractor
4
4
  # @return [String] the ActiveInteractor version
5
- VERSION = '1.0.0.beta.2'
5
+ VERSION = '1.0.0.beta.3'
6
6
  end
@@ -6,6 +6,14 @@ RSpec.describe ActiveInteractor::Config do
6
6
  subject { described_class.new }
7
7
  it { is_expected.to respond_to :logger }
8
8
 
9
+ describe '.defaults' do
10
+ subject { described_class.defaults }
11
+
12
+ it 'is expected to have attributes :logger => Logger.new' do
13
+ expect(subject[:logger]).to be_a Logger
14
+ end
15
+ end
16
+
9
17
  describe '.rails' do
10
18
  subject { described_class.new.rails }
11
19
 
@@ -133,15 +133,137 @@ RSpec.describe ActiveInteractor::Context::Base do
133
133
  end
134
134
  end
135
135
 
136
+ describe '#failure?' do
137
+ subject { instance.failure? }
138
+ let(:instance) { described_class.new }
139
+
140
+ it { is_expected.to eq false }
141
+
142
+ context 'when context has failed' do
143
+ before { instance.instance_variable_set('@_failed', true) }
144
+
145
+ it { is_expected.to eq true }
146
+ end
147
+ end
148
+
149
+ describe '#merge' do
150
+ subject { instance.merge!(attributes) }
151
+
152
+ context 'with an instance having attributes { :foo => "foo"}' do
153
+ let(:instance) { described_class.new(foo: 'foo') }
154
+
155
+ context 'with a hash having attributes { :bar => "bar"}' do
156
+ let(:attributes) { { bar: 'bar' } }
157
+
158
+ it { is_expected.to be_a described_class }
159
+ it { is_expected.to have_attributes(foo: 'foo', bar: 'bar') }
160
+ end
161
+
162
+ context 'with a hash having attributes { :foo => "foobar"}' do
163
+ let(:attributes) { { foo: 'foobar' } }
164
+
165
+ it { is_expected.to be_a described_class }
166
+ it { is_expected.to have_attributes(foo: 'foobar') }
167
+ end
168
+
169
+ context 'with a previous instance having attributes { :bar => "bar" }' do
170
+ let(:attributes) { described_class.new(bar: 'bar') }
171
+
172
+ it { is_expected.to be_a described_class }
173
+ it { is_expected.to have_attributes(foo: 'foo', bar: 'bar') }
174
+
175
+ context 'having errors on :foo' do
176
+ before { attributes.errors.add(:foo, 'invalid') }
177
+
178
+ it 'is expected to have errors on :foo' do
179
+ expect(subject.errors[:foo]).not_to be_nil
180
+ expect(subject.errors[:foo]).to include 'invalid'
181
+ end
182
+ end
183
+
184
+ context 'having instance variable @_failed equal to true' do
185
+ before { attributes.instance_variable_set('@_failed', true) }
186
+
187
+ it { is_expected.to be_a described_class }
188
+ it { is_expected.to have_attributes(foo: 'foo') }
189
+ it 'is expected to preserve @_failed instance variable' do
190
+ expect(subject.instance_variable_get('@_failed')).to eq true
191
+ end
192
+ end
193
+
194
+ context 'having instance variable @_rolled_back equal to true' do
195
+ before { attributes.instance_variable_set('@_rolled_back', true) }
196
+
197
+ it { is_expected.to be_a described_class }
198
+ it { is_expected.to have_attributes(foo: 'foo') }
199
+ it 'is expected to preserve @_rolled_back instance variable' do
200
+ expect(subject.instance_variable_get('@_rolled_back')).to eq true
201
+ end
202
+ end
203
+ end
204
+
205
+ context 'with a previous instance having attributes { :foo => "foobar"}' do
206
+ let(:attributes) { described_class.new(foo: 'foobar') }
207
+
208
+ it { is_expected.to be_a described_class }
209
+ it { is_expected.to have_attributes(foo: 'foobar') }
210
+ end
211
+
212
+ context 'having errors on :foo' do
213
+ before { instance.errors.add(:foo, 'invalid') }
214
+
215
+ context 'with a previous instance having attributes { :bar => "bar" }' do
216
+ let(:attributes) { described_class.new(bar: 'bar') }
217
+
218
+ it 'is expected to have errors on :foo' do
219
+ expect(subject.errors[:foo]).not_to be_nil
220
+ expect(subject.errors[:foo]).to include 'invalid'
221
+ end
222
+
223
+ context 'having errors on :bar' do
224
+ before { attributes.errors.add(:bar, 'invalid') }
225
+
226
+ it 'is expected to have errors on :foo' do
227
+ expect(subject.errors[:foo]).not_to be_nil
228
+ expect(subject.errors[:foo]).to include 'invalid'
229
+ end
230
+
231
+ it 'is expected to have errors on :bar' do
232
+ expect(subject.errors[:bar]).not_to be_nil
233
+ expect(subject.errors[:bar]).to include 'invalid'
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
240
+
136
241
  describe '#new' do
137
242
  subject { described_class.new(attributes) }
138
243
 
244
+ context 'with a hash having attributes { :foo => "foo"}' do
245
+ let(:attributes) { { foo: 'foo' } }
246
+
247
+ it { is_expected.to be_a described_class }
248
+ it { is_expected.to have_attributes(foo: 'foo') }
249
+ end
250
+
139
251
  context 'with a previous instance having attributes { :foo => "foo" }' do
140
252
  let(:attributes) { described_class.new(foo: 'foo') }
141
253
 
142
254
  it { is_expected.to be_a described_class }
143
255
  it { is_expected.to have_attributes(foo: 'foo') }
144
256
 
257
+ context 'having errors on :foo' do
258
+ before { attributes.errors.add(:foo, 'invalid') }
259
+
260
+ it { is_expected.to be_a described_class }
261
+ it 'is expected to have errors on :foo' do
262
+ expect(subject.errors[:foo]).not_to be_nil
263
+ expect(subject.errors[:foo]).to include 'invalid'
264
+ end
265
+ end
266
+
145
267
  context 'having instance variable @_called equal to ["foo"]' do
146
268
  before { attributes.instance_variable_set('@_called', %w[foo]) }
147
269
 
@@ -174,19 +296,6 @@ RSpec.describe ActiveInteractor::Context::Base do
174
296
  end
175
297
  end
176
298
 
177
- describe '#failure?' do
178
- subject { instance.failure? }
179
- let(:instance) { described_class.new }
180
-
181
- it { is_expected.to eq false }
182
-
183
- context 'when context has failed' do
184
- before { instance.instance_variable_set('@_failed', true) }
185
-
186
- it { is_expected.to eq true }
187
- end
188
- end
189
-
190
299
  describe '#rollback!' do
191
300
  subject { instance.rollback! }
192
301
  let(:instance) { described_class.new }
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'active_interactor/interactor/worker'
4
5
 
5
6
  RSpec.describe ActiveInteractor::Interactor::Worker do
6
7
  context 'with interactor class TestInteractor' do
@@ -39,11 +40,31 @@ RSpec.describe ActiveInteractor::Interactor::Worker do
39
40
  subject
40
41
  end
41
42
 
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
+
42
60
  context 'when interactor context is invalid on :calling' do
43
61
  before do
44
62
  allow_any_instance_of(TestInteractor.context_class).to receive(:valid?)
45
63
  .with(:calling)
46
64
  .and_return(false)
65
+ allow_any_instance_of(TestInteractor.context_class).to receive(:valid?)
66
+ .with(:called)
67
+ .and_return(true)
47
68
  end
48
69
 
49
70
  it { expect { subject }.to raise_error(ActiveInteractor::Error::ContextFailure) }
@@ -51,6 +72,33 @@ RSpec.describe ActiveInteractor::Interactor::Worker do
51
72
  expect_any_instance_of(TestInteractor).to receive(:context_rollback!)
52
73
  expect { subject }.to raise_error(ActiveInteractor::Error::ContextFailure)
53
74
  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
54
102
  end
55
103
 
56
104
  context 'when interactor context is invalid on :called' do
@@ -68,6 +116,33 @@ RSpec.describe ActiveInteractor::Interactor::Worker do
68
116
  expect_any_instance_of(TestInteractor).to receive(:context_rollback!)
69
117
  expect { subject }.to raise_error(ActiveInteractor::Error::ContextFailure)
70
118
  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
71
146
  end
72
147
  end
73
148
 
@@ -84,6 +159,30 @@ RSpec.describe ActiveInteractor::Interactor::Worker do
84
159
  expect_any_instance_of(TestInteractor).to receive(:context_rollback!)
85
160
  subject
86
161
  end
162
+
163
+ context 'with options :skip_rollback eq to true' do
164
+ subject { described_class.new(interactor).execute_rollback(skip_rollback: true) }
165
+
166
+ it 'is expected not to call #context_rollback on interactor instance' do
167
+ expect_any_instance_of(TestInteractor).not_to receive(:context_rollback!)
168
+ subject
169
+ end
170
+ end
171
+
172
+ context 'with options :skip_rollback_callbacks eq to true' do
173
+ subject { described_class.new(interactor).execute_rollback(skip_rollback_callbacks: true) }
174
+
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)
178
+ subject
179
+ end
180
+
181
+ it 'calls #context_rollback on interactor instance' do
182
+ expect_any_instance_of(TestInteractor).to receive(:context_rollback!)
183
+ subject
184
+ end
185
+ end
87
186
  end
88
187
  end
89
188
  end
@@ -136,6 +136,7 @@ RSpec.describe ActiveInteractor::Organizer do
136
136
  end
137
137
 
138
138
  it { expect { subject }.not_to raise_error }
139
+ it { is_expected.to be_failure }
139
140
  it { is_expected.to be_a interactor_class.context_class }
140
141
  it 'is expected to call #perform on the first interactor' do
141
142
  expect_any_instance_of(interactor1).to receive(:perform)
@@ -161,6 +162,7 @@ RSpec.describe ActiveInteractor::Organizer do
161
162
  end
162
163
 
163
164
  it { expect { subject }.not_to raise_error }
165
+ it { is_expected.to be_failure }
164
166
  it { is_expected.to be_a interactor_class.context_class }
165
167
  it 'is expected to call #perform on both interactors' do
166
168
  expect_any_instance_of(interactor1).to receive(:perform)
@@ -173,6 +175,71 @@ RSpec.describe ActiveInteractor::Organizer do
173
175
  subject
174
176
  end
175
177
  end
178
+
179
+ context 'when the organizer is set to perform in parallel' do
180
+ let(:interactor_class) do
181
+ build_organizer do
182
+ perform_in_parallel
183
+
184
+ organize TestInteractor1, TestInteractor2
185
+ end
186
+ end
187
+
188
+ it { is_expected.to be_a interactor_class.context_class }
189
+ it 'is expected to call #perform on both interactors' do
190
+ expect_any_instance_of(interactor1).to receive(:perform)
191
+ expect_any_instance_of(interactor2).to receive(:perform)
192
+ subject
193
+ end
194
+
195
+ context 'when the first interactor context fails' do
196
+ let!(:interactor1) do
197
+ build_interactor('TestInteractor1') do
198
+ def perform
199
+ context.fail!
200
+ end
201
+ end
202
+ end
203
+
204
+ it { expect { subject }.not_to raise_error }
205
+ it { is_expected.to be_failure }
206
+ it { is_expected.to be_a interactor_class.context_class }
207
+ it 'is expected to call #perform on both interactors' do
208
+ expect_any_instance_of(interactor1).to receive(:perform)
209
+ expect_any_instance_of(interactor2).to receive(:perform)
210
+ subject
211
+ end
212
+ it 'is expected to call #rollback both interactors' do
213
+ expect_any_instance_of(interactor1).to receive(:rollback)
214
+ expect_any_instance_of(interactor2).to receive(:rollback)
215
+ subject
216
+ end
217
+ end
218
+
219
+ context 'when the second interactor context fails' do
220
+ let!(:interactor2) do
221
+ build_interactor('TestInteractor2') do
222
+ def perform
223
+ context.fail!
224
+ end
225
+ end
226
+ end
227
+
228
+ it { expect { subject }.not_to raise_error }
229
+ it { is_expected.to be_failure }
230
+ it { is_expected.to be_a interactor_class.context_class }
231
+ it 'is expected to call #perform on both interactors' do
232
+ expect_any_instance_of(interactor1).to receive(:perform)
233
+ expect_any_instance_of(interactor2).to receive(:perform)
234
+ subject
235
+ end
236
+ it 'is expected to call #rollback on both interactors' do
237
+ expect_any_instance_of(interactor1).to receive(:rollback)
238
+ expect_any_instance_of(interactor2).to receive(:rollback)
239
+ subject
240
+ end
241
+ end
242
+ end
176
243
  end
177
244
  end
178
245
  end
@@ -8,4 +8,16 @@ RSpec.describe ActiveInteractor::Rails::Config do
8
8
 
9
9
  it { is_expected.to respond_to :directory }
10
10
  it { is_expected.to respond_to :generate_context_classes }
11
+
12
+ describe '.defaults' do
13
+ subject { described_class.defaults }
14
+
15
+ it 'is expected to have attributes :directory => "interactors"' do
16
+ expect(subject[:directory]).to eq 'interactors'
17
+ end
18
+
19
+ it 'is expected to have attributes :generate_context_classes => true' do
20
+ expect(subject[:generate_context_classes]).to eq true
21
+ end
22
+ end
11
23
  end
@@ -217,4 +217,55 @@ RSpec.describe 'Basic Integration', type: :integration do
217
217
  end
218
218
  end
219
219
  end
220
+
221
+ describe 'A basic organizer performing in parallel' do
222
+ let!(:test_interactor_1) do
223
+ build_interactor('TestInteractor1') do
224
+ def perform
225
+ context.test_field_1 = 'test 1'
226
+ end
227
+ end
228
+ end
229
+
230
+ let!(:test_interactor_2) do
231
+ build_interactor('TestInteractor2') do
232
+ def perform
233
+ context.test_field_2 = 'test 2'
234
+ end
235
+ end
236
+ end
237
+
238
+ let(:interactor_class) do
239
+ build_organizer do
240
+ perform_in_parallel
241
+ organize TestInteractor1, TestInteractor2
242
+ end
243
+ end
244
+
245
+ include_examples 'a class with interactor methods'
246
+ include_examples 'a class with interactor callback methods'
247
+ include_examples 'a class with interactor context methods'
248
+ include_examples 'a class with organizer callback methods'
249
+
250
+ describe '.context_class' do
251
+ subject { interactor_class.context_class }
252
+
253
+ it { is_expected.to eq TestOrganizer::Context }
254
+ it { is_expected.to be < ActiveInteractor::Context::Base }
255
+ end
256
+
257
+ describe '.organized' do
258
+ subject { interactor_class.organized }
259
+
260
+ it { is_expected.to eq [TestInteractor1, TestInteractor2] }
261
+ end
262
+
263
+ describe '.perform' do
264
+ subject { interactor_class.perform }
265
+
266
+ it { is_expected.to be_a interactor_class.context_class }
267
+ it { is_expected.to be_successful }
268
+ it { is_expected.to have_attributes(test_field_1: 'test 1', test_field_2: 'test 2') }
269
+ end
270
+ end
220
271
  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.0.beta.2
4
+ version: 1.0.0.beta.3
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-01-07 00:00:00.000000000 Z
11
+ date: 2020-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -107,6 +107,7 @@ files:
107
107
  - lib/active_interactor.rb
108
108
  - lib/active_interactor/base.rb
109
109
  - lib/active_interactor/config.rb
110
+ - lib/active_interactor/configurable.rb
110
111
  - lib/active_interactor/context.rb
111
112
  - lib/active_interactor/context/attributes.rb
112
113
  - lib/active_interactor/context/base.rb
@@ -115,6 +116,7 @@ files:
115
116
  - lib/active_interactor/interactor.rb
116
117
  - lib/active_interactor/interactor/callbacks.rb
117
118
  - lib/active_interactor/interactor/context.rb
119
+ - lib/active_interactor/interactor/perform_options.rb
118
120
  - lib/active_interactor/interactor/worker.rb
119
121
  - lib/active_interactor/organizer.rb
120
122
  - lib/active_interactor/rails.rb
@@ -166,10 +168,10 @@ licenses:
166
168
  - MIT
167
169
  metadata:
168
170
  bug_tracker_uri: https://github.com/aaronmallen/activeinteractor/issues
169
- changelog_uri: https://github.com/aaronmallen/activeinteractor/blob/v1.0.0.beta.2/CHANGELOG.md
170
- documentation_uri: https://www.rubydoc.info/gems/activeinteractor/1.0.0.beta.2
171
+ changelog_uri: https://github.com/aaronmallen/activeinteractor/blob/v1.0.0.beta.3/CHANGELOG.md
172
+ documentation_uri: https://www.rubydoc.info/gems/activeinteractor/1.0.0.beta.3
171
173
  hompage_uri: https://github.com/aaronmallen/activeinteractor
172
- source_code_uri: https://github.com/aaronmallen/activeinteractor/tree/v1.0.0.beta.2
174
+ source_code_uri: https://github.com/aaronmallen/activeinteractor/tree/v1.0.0.beta.3
173
175
  wiki_uri: https://github.com/aaronmallen/activeinteractor/wiki
174
176
  post_install_message:
175
177
  rdoc_options: []