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.
@@ -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