activeinteractor 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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