activeinteractor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE +21 -0
- data/README.md +661 -0
- data/lib/active_interactor.rb +63 -0
- data/lib/active_interactor/base.rb +18 -0
- data/lib/active_interactor/configuration.rb +34 -0
- data/lib/active_interactor/context.rb +300 -0
- data/lib/active_interactor/interactor.rb +97 -0
- data/lib/active_interactor/interactor/callbacks.rb +273 -0
- data/lib/active_interactor/interactor/context.rb +59 -0
- data/lib/active_interactor/interactor/execution.rb +25 -0
- data/lib/active_interactor/interactor/worker.rb +89 -0
- data/lib/active_interactor/organizer.rb +47 -0
- data/lib/active_interactor/version.rb +7 -0
- data/lib/rails/generators/active_interactor.rb +35 -0
- data/lib/rails/generators/active_interactor/install_generator.rb +31 -0
- data/lib/rails/generators/interactor/interactor_generator.rb +13 -0
- data/lib/rails/generators/interactor/organizer_generator.rb +18 -0
- data/lib/rails/generators/interactor/rspec_generator.rb +15 -0
- data/lib/rails/generators/interactor/test_unit_generator.rb +15 -0
- data/lib/rails/generators/templates/application_interactor.erb +4 -0
- data/lib/rails/generators/templates/initializer.rb +8 -0
- data/lib/rails/generators/templates/interactor.erb +12 -0
- data/lib/rails/generators/templates/organizer.erb +10 -0
- data/lib/rails/generators/templates/rspec.erb +7 -0
- data/lib/rails/generators/templates/test_unit.erb +9 -0
- metadata +269 -0
@@ -0,0 +1,273 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/array/extract_options'
|
4
|
+
require 'active_support/core_ext/class/attribute'
|
5
|
+
|
6
|
+
module ActiveInteractor
|
7
|
+
module Interactor
|
8
|
+
# Provides context attribute assignment methods to included classes
|
9
|
+
#
|
10
|
+
# @author Aaron Allen <hello@aaronmallen.me>
|
11
|
+
# @since 0.0.1
|
12
|
+
# @version 0.1
|
13
|
+
module Callbacks
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
extend ClassMethods
|
18
|
+
include ActiveSupport::Callbacks
|
19
|
+
|
20
|
+
class_attribute :__clean_after_perform, instance_writer: false, default: false
|
21
|
+
class_attribute :__fail_on_invalid_context, instance_writer: false, default: true
|
22
|
+
define_callbacks :validation,
|
23
|
+
skip_after_callbacks_if_terminated: true,
|
24
|
+
scope: %i[kind name]
|
25
|
+
define_callbacks :perform, :rollback
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
# Define a callback to call after `#valid?` has been invoked on an
|
30
|
+
# interactor's context
|
31
|
+
#
|
32
|
+
# @example Implement an after_context_validation callback
|
33
|
+
# class MyInteractor < ActiveInteractor::Base
|
34
|
+
# after_context_validation :ensure_name_is_aaron
|
35
|
+
# context_validates :name, inclusion: { in: %w[Aaron] }
|
36
|
+
#
|
37
|
+
# def ensure_name_is_aaron
|
38
|
+
# context.name = 'Aaron'
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# context = MyInteractor.new(name: 'Bob').context
|
43
|
+
# #=> <MyInteractor::Context name='Bob'>
|
44
|
+
#
|
45
|
+
# context.valid?
|
46
|
+
# #=> false
|
47
|
+
#
|
48
|
+
# context.name
|
49
|
+
# #=> 'Aaron'
|
50
|
+
#
|
51
|
+
# context.valid?
|
52
|
+
# #=> true
|
53
|
+
def after_context_validation(*args, &block)
|
54
|
+
options = normalize_options(args.extract_options!.dup.merge(prepend: true))
|
55
|
+
set_callback(:validation, :after, *args, options, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Define a callback to call after {ActiveInteractor::Base.perform} has been invoked
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# class MyInteractor < ActiveInteractor::Base
|
62
|
+
# after_perform :print_done
|
63
|
+
#
|
64
|
+
# def perform
|
65
|
+
# puts 'Performing'
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# def print_done
|
69
|
+
# puts 'Done'
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# MyInteractor.perform(name: 'Aaron')
|
74
|
+
# "Performing"
|
75
|
+
# "Done"
|
76
|
+
# #=> <MyInteractor::Context name='Aaron'>
|
77
|
+
def after_perform(*filters, &block)
|
78
|
+
set_callback(:perform, :after, *filters, &block)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Define a callback to call after {ActiveInteractor::Base#rollback} has been invoked
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# class MyInteractor < ActiveInteractor::Base
|
85
|
+
# after_rollback :print_done
|
86
|
+
#
|
87
|
+
# def rollback
|
88
|
+
# puts 'Rolling back'
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# def print_done
|
92
|
+
# puts 'Done'
|
93
|
+
# end
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# context = MyInteractor.perform(name: 'Aaron')
|
97
|
+
# #=> <MyInteractor::Context name='Aaron'>
|
98
|
+
#
|
99
|
+
# context.rollback!
|
100
|
+
# "Rolling back"
|
101
|
+
# "Done"
|
102
|
+
def after_rollback(*filters, &block)
|
103
|
+
set_callback(:rollback, :after, *filters, &block)
|
104
|
+
end
|
105
|
+
|
106
|
+
# By default an interactor context will fail if it is deemed
|
107
|
+
# invalid before or after the {ActiveInteractor::Base.perform} method
|
108
|
+
# is invoked. Calling this method on an interactor class
|
109
|
+
# will not invoke {ActiveInteractor::Context::Base#fail!} if the
|
110
|
+
# context is invalid.
|
111
|
+
def allow_context_to_be_invalid
|
112
|
+
self.__fail_on_invalid_context = false
|
113
|
+
end
|
114
|
+
|
115
|
+
# Define a callback to call around {ActiveInteractor::Base.perform} invokation
|
116
|
+
#
|
117
|
+
# @example
|
118
|
+
# class MyInteractor < ActiveInteractor::Base
|
119
|
+
# around_perform :track_time
|
120
|
+
#
|
121
|
+
# def perform
|
122
|
+
# sleep(1)
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# def track_time
|
126
|
+
# context.start_time = Time.now.utc
|
127
|
+
# yield
|
128
|
+
# context.end_time = Time.now.utc
|
129
|
+
# end
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# context = MyInteractor.perform(name: 'Aaron')
|
133
|
+
# #=> <MyInteractor::Context name='Aaron'>
|
134
|
+
#
|
135
|
+
# context.start_time
|
136
|
+
# #=> 2019-01-01 00:00:00 UTC
|
137
|
+
#
|
138
|
+
# context.end_time
|
139
|
+
# #=> 2019-01-01 00:00:01 UTC
|
140
|
+
def around_perform(*filters, &block)
|
141
|
+
set_callback(:perform, :around, *filters, &block)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Define a callback to call around {ActiveInteractor::Base#rollback} invokation
|
145
|
+
#
|
146
|
+
# @example
|
147
|
+
# class MyInteractor < ActiveInteractor::Base
|
148
|
+
# around_rollback :track_time
|
149
|
+
#
|
150
|
+
# def rollback
|
151
|
+
# sleep(1)
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
# def track_time
|
155
|
+
# context.start_time = Time.now.utc
|
156
|
+
# yield
|
157
|
+
# context.end_time = Time.now.utc
|
158
|
+
# end
|
159
|
+
# end
|
160
|
+
#
|
161
|
+
# context = MyInteractor.perform(name: 'Aaron')
|
162
|
+
# #=> <MyInteractor::Context name='Aaron'>
|
163
|
+
#
|
164
|
+
# context.rollback!
|
165
|
+
# #=> true
|
166
|
+
#
|
167
|
+
# context.start_time
|
168
|
+
# #=> 2019-01-01 00:00:00 UTC
|
169
|
+
#
|
170
|
+
# context.end_time
|
171
|
+
# #=> 2019-01-01 00:00:01 UTC
|
172
|
+
def around_rollback(*filters, &block)
|
173
|
+
set_callback(:rollback, :around, *filters, &block)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Define a callback to call before `#valid?` has been invoked on an
|
177
|
+
# interactor's context
|
178
|
+
#
|
179
|
+
# @example Implement an after_context_validation callback
|
180
|
+
# class MyInteractor < ActiveInteractor::Base
|
181
|
+
# before_context_validation :set_name_aaron
|
182
|
+
# context_validates :name, inclusion: { in: %w[Aaron] }
|
183
|
+
#
|
184
|
+
# def set_name_aaron
|
185
|
+
# context.name = 'Aaron'
|
186
|
+
# end
|
187
|
+
# end
|
188
|
+
#
|
189
|
+
# context = MyInteractor.new(name: 'Bob').context
|
190
|
+
# #=> <MyInteractor::Context name='Bob'>
|
191
|
+
#
|
192
|
+
# context.valid?
|
193
|
+
# #=> true
|
194
|
+
#
|
195
|
+
# context.name
|
196
|
+
# #=> 'Aaron'
|
197
|
+
def before_context_validation(*args, &block)
|
198
|
+
options = normalize_options(args.extract_options!.dup)
|
199
|
+
set_callback(:validation, :before, *args, options, &block)
|
200
|
+
end
|
201
|
+
|
202
|
+
# Define a callback to call before {ActiveInteractor::Base.perform} has been invoked
|
203
|
+
#
|
204
|
+
# @example
|
205
|
+
# class MyInteractor < ActiveInteractor::Base
|
206
|
+
# before_perform :print_start
|
207
|
+
#
|
208
|
+
# def perform
|
209
|
+
# puts 'Performing'
|
210
|
+
# end
|
211
|
+
#
|
212
|
+
# def print_start
|
213
|
+
# puts 'Start'
|
214
|
+
# end
|
215
|
+
# end
|
216
|
+
#
|
217
|
+
# MyInteractor.perform(name: 'Aaron')
|
218
|
+
# "Start"
|
219
|
+
# "Performing"
|
220
|
+
# #=> <MyInteractor::Context name='Aaron'>
|
221
|
+
def before_perform(*filters, &block)
|
222
|
+
set_callback(:perform, :before, *filters, &block)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Define a callback to call before {ActiveInteractor::Base#rollback} has been invoked
|
226
|
+
#
|
227
|
+
# @example
|
228
|
+
# class MyInteractor < ActiveInteractor::Base
|
229
|
+
# before_rollback :print_start
|
230
|
+
#
|
231
|
+
# def rollback
|
232
|
+
# puts 'Rolling Back'
|
233
|
+
# end
|
234
|
+
#
|
235
|
+
# def print_start
|
236
|
+
# puts 'Start'
|
237
|
+
# end
|
238
|
+
# end
|
239
|
+
#
|
240
|
+
# context = MyInteractor.perform(name: 'Aaron')
|
241
|
+
# #=> <MyInteractor::Context name='Aaron'>
|
242
|
+
#
|
243
|
+
# context.rollback!
|
244
|
+
# "Start"
|
245
|
+
# "Rolling Back"
|
246
|
+
# #=> true
|
247
|
+
def before_rollback(*filters, &block)
|
248
|
+
set_callback(:rollback, :before, *filters, &block)
|
249
|
+
end
|
250
|
+
|
251
|
+
# Calling this method on an interactor class will invoke
|
252
|
+
# {ActiveInteractor::Context::Base#clean!} on the interactor's
|
253
|
+
# context instance after {ActiveInteractor::Base.perform}
|
254
|
+
# is invoked.
|
255
|
+
def clean_context_on_completion
|
256
|
+
self.__clean_after_perform = true
|
257
|
+
end
|
258
|
+
|
259
|
+
private
|
260
|
+
|
261
|
+
def normalize_options(options)
|
262
|
+
if options.key?(:on)
|
263
|
+
options[:on] = Array(options[:on])
|
264
|
+
options[:if] = Array(options[:if])
|
265
|
+
options[:if].unshift { |o| !(options[:on] & Array(o.validation_context)).empty? }
|
266
|
+
end
|
267
|
+
|
268
|
+
options
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInteractor
|
4
|
+
module Interactor
|
5
|
+
# Provides ActiveInteractor::Interactor::Context methods to included classes
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
# @author Aaron Allen <hello@aaronmallen.me>
|
9
|
+
# @since 0.0.1
|
10
|
+
# @version 0.1
|
11
|
+
module Context
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
included do
|
15
|
+
extend ClassMethods
|
16
|
+
include Callbacks
|
17
|
+
|
18
|
+
delegate(*ActiveModel::Validations.instance_methods, to: :context, prefix: true)
|
19
|
+
delegate(*ActiveModel::Validations::HelperMethods.instance_methods, to: :context, prefix: true)
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
delegate(*ActiveModel::Validations::ClassMethods.instance_methods, to: :context_class, prefix: :context)
|
24
|
+
delegate(*ActiveModel::Validations::HelperMethods.instance_methods, to: :context_class, prefix: :context)
|
25
|
+
|
26
|
+
# Create the context class for inherited classes.
|
27
|
+
def inherited(base)
|
28
|
+
base.const_set 'Context', Class.new(ActiveInteractor::Context::Base)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Assign attributes to the context class of the interactor
|
32
|
+
#
|
33
|
+
# @example Assign attributes to the context class
|
34
|
+
# class MyInteractor > ActiveInteractor::Base
|
35
|
+
# context_attributes :first_name, :last_name
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# MyInteractor::Context.attributes
|
39
|
+
# #=> [:first_name, :last_name]
|
40
|
+
# @param attributes [Array] the attributes to assign to the context
|
41
|
+
def context_attributes(*attributes)
|
42
|
+
context_class.attributes = attributes
|
43
|
+
end
|
44
|
+
|
45
|
+
# The context class of the interactor
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# class MyInteractor < ActiveInteractor::Base
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# MyInteractor.context_class
|
52
|
+
# #=> MyInteractor::Context
|
53
|
+
def context_class
|
54
|
+
const_get 'Context'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInteractor
|
4
|
+
module Interactor
|
5
|
+
module Execution
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
DELEGATED_WORKER_METHODS = %i[
|
9
|
+
execute_perform
|
10
|
+
execute_perform!
|
11
|
+
execute_rollback
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
included do
|
15
|
+
delegate(*DELEGATED_WORKER_METHODS, to: :worker)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def worker
|
21
|
+
Worker.new(self)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInteractor
|
4
|
+
module Interactor
|
5
|
+
class Worker
|
6
|
+
delegate :run_callbacks, to: :interactor
|
7
|
+
|
8
|
+
# A new instance of {Worker}
|
9
|
+
# @param interactor [ActiveInteractor::Base] an interactor instance
|
10
|
+
# @return [ActiveInteractor::Interactor::Worker] a new instance of {Worker}
|
11
|
+
def initialize(interactor)
|
12
|
+
@interactor = clone_interactor(interactor)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Calls {#execute_perform!} and rescues {ActiveInteractor::Context::Failure}
|
16
|
+
# @return [ActiveInteractor::Context::Base] an instance of {ActiveInteractor::Context::Base}
|
17
|
+
def execute_perform
|
18
|
+
execute_perform!
|
19
|
+
rescue ActiveInteractor::Context::Failure => exception
|
20
|
+
ActiveInteractor.logger.error("ActiveInteractor: #{exception}")
|
21
|
+
context
|
22
|
+
end
|
23
|
+
|
24
|
+
# Calls {Interactor#perform} with callbacks and context validation
|
25
|
+
# @raise [ActiveInteractor::Context::Failure] if the context fails
|
26
|
+
# @return [ActiveInteractor::Context::Base] an instance of {ActiveInteractor::Context::Base}
|
27
|
+
def execute_perform!
|
28
|
+
run_callbacks :perform do
|
29
|
+
perform!
|
30
|
+
finalize_context!
|
31
|
+
context
|
32
|
+
rescue # rubocop:disable Style/RescueStandardError
|
33
|
+
context.rollback!
|
34
|
+
raise
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Calls {Interactor#rollback} with callbacks
|
39
|
+
def execute_rollback
|
40
|
+
run_callbacks :rollback do
|
41
|
+
interactor.rollback
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :interactor
|
48
|
+
|
49
|
+
def clone_interactor(interactor)
|
50
|
+
cloned = interactor.clone
|
51
|
+
cloned.send(:context=, cloned.send(:context).clone)
|
52
|
+
cloned
|
53
|
+
end
|
54
|
+
|
55
|
+
def context
|
56
|
+
interactor.send(:context)
|
57
|
+
end
|
58
|
+
|
59
|
+
def fail_on_invalid_context!(validation_context = nil)
|
60
|
+
context.fail! if should_fail_on_invalid_context?(validation_context)
|
61
|
+
end
|
62
|
+
|
63
|
+
def finalize_context!
|
64
|
+
context.clean! if interactor.should_clean_context?
|
65
|
+
context.called!
|
66
|
+
end
|
67
|
+
|
68
|
+
def perform!
|
69
|
+
fail_on_invalid_context!(:calling)
|
70
|
+
interactor.perform
|
71
|
+
fail_on_invalid_context!(:called)
|
72
|
+
end
|
73
|
+
|
74
|
+
def should_fail_on_invalid_context?(validation_context)
|
75
|
+
!validate_context(validation_context) && interactor.fail_on_invalid_context?
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate_context(validation_context = nil)
|
79
|
+
run_callbacks :validation do
|
80
|
+
init_validation_context = context.validation_context
|
81
|
+
context.validation_context = validation_context || init_validation_context
|
82
|
+
context.valid?
|
83
|
+
ensure
|
84
|
+
context.validation_context = init_validation_context
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|