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

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: 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: []