interactor_with_steroids 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,139 @@
1
+ require "active_support/core_ext/module/delegation"
2
+
3
+ module Interactor
4
+ # Public: The object for tracking state of an Interactor's invocation. The
5
+ # context is used to initialize the interactor with the information required
6
+ # for invocation. The interactor manipulates the context to produce the result
7
+ # of invocation.
8
+ #
9
+ # The context is the mechanism by which success and failure are determined and
10
+ # the context is responsible for tracking individual interactor invocations
11
+ # for the purpose of rollback.
12
+ #
13
+ # The context may be manipulated using arbitrary getter and setter methods.
14
+ #
15
+ # Examples
16
+ #
17
+ # context = Interactor::Context.new
18
+ # # => #<Interactor::Context>
19
+ # context.foo = "bar"
20
+ # # => "bar"
21
+ # context
22
+ # # => #<Interactor::Context foo="bar">
23
+ # context.hello = "world"
24
+ # # => "world"
25
+ # context
26
+ # # => #<Interactor::Context foo="bar" hello="world">
27
+ # context.foo = "baz"
28
+ # # => "baz"
29
+ # context
30
+ # # => #<Interactor::Context foo="baz" hello="world">
31
+ class Context
32
+ # Internal: Initialize an Interactor::Context or preserve an existing one.
33
+ # If the argument given is an Interactor::Context, the argument is returned.
34
+ # Otherwise, a new Interactor::Context is initialized from the provided
35
+ # hash.
36
+ #
37
+ # The "build" method is used during interactor initialization.
38
+ #
39
+ # context - A Hash whose key/value pairs are used in initializing a new
40
+ # Interactor::Context object. If an existing Interactor::Context
41
+ # is given, it is simply returned. (default: {})
42
+ #
43
+ # Examples
44
+ #
45
+ # context = Interactor::Context.build(foo: "bar")
46
+ # # => #<Interactor::Context foo="bar">
47
+ # context.object_id
48
+ # # => 2170969340
49
+ # context = Interactor::Context.build(context)
50
+ # # => #<Interactor::Context foo="bar">
51
+ # context.object_id
52
+ # # => 2170969340
53
+ #
54
+ # Returns the Interactor::Context.
55
+ def self.build(context = {})
56
+ new(**context.to_h)
57
+ end
58
+
59
+ attr_accessor :error
60
+ delegate :to_s, to: :to_h
61
+
62
+ def initialize(*)
63
+ end
64
+
65
+ # Public: Whether the Interactor::Context is successful. By default, a new
66
+ # context is successful and only changes when explicitly failed.
67
+ #
68
+ # The "success?" method is the inverse of the "failure?" method.
69
+ #
70
+ # Examples
71
+ #
72
+ # context = Interactor::Context.new
73
+ # # => #<Interactor::Context>
74
+ # context.success?
75
+ # # => true
76
+ # context.fail!
77
+ # # => Interactor::Failure: #<Interactor::Context>
78
+ # context.success?
79
+ # # => false
80
+ #
81
+ # Returns true by default or false if failed.
82
+ def success?
83
+ !failure?
84
+ end
85
+
86
+ # Public: Whether the Interactor::Context has failed. By default, a new
87
+ # context is successful and only changes when explicitly failed.
88
+ #
89
+ # The "failure?" method is the inverse of the "success?" method.
90
+ #
91
+ # Examples
92
+ #
93
+ # context = Interactor::Context.new
94
+ # # => #<Interactor::Context>
95
+ # context.failure?
96
+ # # => false
97
+ # context.fail!
98
+ # # => Interactor::Failure: #<Interactor::Context>
99
+ # context.failure?
100
+ # # => true
101
+ #
102
+ # Returns false by default or true if failed.
103
+ def failure?
104
+ @failure || false
105
+ end
106
+
107
+ # Public: Fail the Interactor::Context. Failing a context raises an error
108
+ # that may be rescued by the calling interactor. The context is also flagged
109
+ # as having failed.
110
+ #
111
+ # Optionally the caller may provide a hash of key/value pairs to be merged
112
+ # into the context before failure.
113
+ #
114
+ # context - A Hash whose key/value pairs are merged into the existing
115
+ # Interactor::Context instance. (default: {})
116
+ #
117
+ # Examples
118
+ #
119
+ # context = Interactor::Context.new
120
+ # # => #<Interactor::Context>
121
+ # context.fail!
122
+ # # => Interactor::Failure: #<Interactor::Context>
123
+ # context.fail! rescue false
124
+ # # => false
125
+ # context.fail!(foo: "baz")
126
+ # # => Interactor::Failure: #<Interactor::Context foo="baz">
127
+ #
128
+ # Raises Interactor::Failure initialized with the Interactor::Context.
129
+ def fail!(error: nil)
130
+ self.error = error
131
+ @failure = true
132
+ raise Failure, self
133
+ end
134
+
135
+ def to_h
136
+ { error: error }
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,85 @@
1
+
2
+ require "active_support/core_ext/module/delegation"
3
+ require "active_support/core_ext/class/attribute"
4
+ require "active_support/concern"
5
+
6
+ module Interactor
7
+ # Internal: Methods relating to declaring what we receive and what will be stored in the context.
8
+ module Declaration
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ class_attribute :context_class, instance_writer: false, default: Context
13
+ end
14
+
15
+ class_methods do
16
+ def receive(*required_arguments, **optional_arguments)
17
+ @required_arguments ||= []
18
+ new_required_arguments = required_arguments - @required_arguments
19
+ @required_arguments += new_required_arguments
20
+
21
+ delegate(*new_required_arguments, to: :context) unless new_required_arguments.empty?
22
+ delegate(*optional_arguments.keys, to: :context) unless optional_arguments.empty?
23
+
24
+ attributes = [*new_required_arguments, *optional_arguments.keys]
25
+
26
+ self.context_class = Class.new(context_class) do
27
+ attr_accessor *new_required_arguments
28
+ attr_writer *optional_arguments.keys
29
+
30
+ optional_arguments.each do |k, v|
31
+ define_method(k) do
32
+ ivar = "@#{k}"
33
+ return instance_variable_get(ivar) if instance_variable_defined?(ivar)
34
+
35
+ instance_variable_set(ivar, v.is_a?(Proc) ? instance_eval(&v) : v)
36
+ end
37
+ end
38
+
39
+ class_eval %Q<
40
+ def initialize(
41
+ #{new_required_arguments.map { |a| "#{a}:" }.join(', ')}#{new_required_arguments.empty? ? '' : ', '}
42
+ **rest
43
+ )
44
+ super(**rest)
45
+
46
+ #{new_required_arguments.map { |a| "self.#{a} = #{a}" }.join(';')}
47
+
48
+ #{
49
+ optional_arguments.keys.map do |k|
50
+ "instance_variable_set('@#{k}', rest[:#{k}]) if rest.key?(:#{k})"
51
+ end.join("\n")
52
+ }
53
+ end
54
+ >
55
+
56
+ class_eval %Q<
57
+ def to_h
58
+ super.merge(
59
+ #{attributes.map { |a| "#{a}: self.#{a}"}.join(', ')}
60
+ )
61
+ end
62
+ >
63
+ end
64
+ end
65
+
66
+ def hold(*held_fields)
67
+ @held_fields ||= []
68
+ @held_fields += held_fields
69
+
70
+ delegate(*@held_fields, to: :context)
71
+
72
+ self.context_class = Class.new(context_class) do
73
+ attr_accessor *held_fields
74
+
75
+ class_eval %Q<
76
+ def to_h
77
+ super.merge(#{held_fields.map { |f| "#{f}: self.#{f}"}.join(', ')})
78
+ end
79
+ >
80
+ end
81
+
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,31 @@
1
+ module Interactor
2
+ # Internal: Error raised during Interactor::Context failure. The error stores
3
+ # a copy of the failed context for debugging purposes.
4
+ class Failure < StandardError
5
+ # Internal: Gets the Interactor::Context of the Interactor::Failure
6
+ # instance.
7
+ attr_reader :context
8
+
9
+ # Internal: Initialize an Interactor::Failure.
10
+ #
11
+ # context - An Interactor::Context to be stored within the
12
+ # Interactor::Failure instance. (default: nil)
13
+ #
14
+ # Examples
15
+ #
16
+ # Interactor::Failure.new
17
+ # # => #<Interactor::Failure: Interactor::Failure>
18
+ #
19
+ # context = Interactor::Context.new(foo: "bar")
20
+ # # => #<Interactor::Context foo="bar">
21
+ # Interactor::Failure.new(context)
22
+ # # => #<Interactor::Failure: #<Interactor::Context foo="bar">>
23
+ #
24
+ # raise Interactor::Failure, context
25
+ # # => Interactor::Failure: #<Interactor::Context foo="bar">
26
+ def initialize(context = nil)
27
+ @context = context
28
+ super
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,263 @@
1
+ require "active_support/concern"
2
+
3
+ module Interactor
4
+ # Internal: Methods relating to supporting hooks around Interactor invocation.
5
+ module Hooks
6
+ extend ActiveSupport::Concern
7
+
8
+ # Internal: Interactor::Hooks class methods.
9
+ class_methods do
10
+ # Public: Declare hooks to run around Interactor invocation. The around
11
+ # method may be called multiple times; subsequent calls append declared
12
+ # hooks to existing around hooks.
13
+ #
14
+ # hooks - Zero or more Symbol method names representing instance methods
15
+ # to be called around interactor invocation. Each instance method
16
+ # invocation receives an argument representing the next link in
17
+ # the around hook chain.
18
+ # block - An optional block to be executed as a hook. If given, the block
19
+ # is executed after methods corresponding to any given Symbols.
20
+ #
21
+ # Examples
22
+ #
23
+ # class MyInteractor
24
+ # include Interactor
25
+ #
26
+ # around :time_execution
27
+ #
28
+ # around do |interactor|
29
+ # puts "started"
30
+ # interactor.call
31
+ # puts "finished"
32
+ # end
33
+ #
34
+ # def call
35
+ # puts "called"
36
+ # end
37
+ #
38
+ # private
39
+ #
40
+ # def time_execution(interactor)
41
+ # context.start_time = Time.now
42
+ # interactor.call
43
+ # context.finish_time = Time.now
44
+ # end
45
+ # end
46
+ #
47
+ # Returns nothing.
48
+ def around(*hooks, &block)
49
+ hooks << block if block
50
+ hooks.each { |hook| around_hooks.push(hook) }
51
+ end
52
+
53
+ # Public: Declare hooks to run before Interactor invocation. The before
54
+ # method may be called multiple times; subsequent calls append declared
55
+ # hooks to existing before hooks.
56
+ #
57
+ # hooks - Zero or more Symbol method names representing instance methods
58
+ # to be called before interactor invocation.
59
+ # block - An optional block to be executed as a hook. If given, the block
60
+ # is executed after methods corresponding to any given Symbols.
61
+ #
62
+ # Examples
63
+ #
64
+ # class MyInteractor
65
+ # include Interactor
66
+ #
67
+ # before :set_start_time
68
+ #
69
+ # before do
70
+ # puts "started"
71
+ # end
72
+ #
73
+ # def call
74
+ # puts "called"
75
+ # end
76
+ #
77
+ # private
78
+ #
79
+ # def set_start_time
80
+ # context.start_time = Time.now
81
+ # end
82
+ # end
83
+ #
84
+ # Returns nothing.
85
+ def before(*hooks, &block)
86
+ hooks << block if block
87
+ hooks.each { |hook| before_hooks.push(hook) }
88
+ end
89
+
90
+ # Public: Declare hooks to run after Interactor invocation. The after
91
+ # method may be called multiple times; subsequent calls prepend declared
92
+ # hooks to existing after hooks.
93
+ #
94
+ # hooks - Zero or more Symbol method names representing instance methods
95
+ # to be called after interactor invocation.
96
+ # block - An optional block to be executed as a hook. If given, the block
97
+ # is executed before methods corresponding to any given Symbols.
98
+ #
99
+ # Examples
100
+ #
101
+ # class MyInteractor
102
+ # include Interactor
103
+ #
104
+ # after :set_finish_time
105
+ #
106
+ # after do
107
+ # puts "finished"
108
+ # end
109
+ #
110
+ # def call
111
+ # puts "called"
112
+ # end
113
+ #
114
+ # private
115
+ #
116
+ # def set_finish_time
117
+ # context.finish_time = Time.now
118
+ # end
119
+ # end
120
+ #
121
+ # Returns nothing.
122
+ def after(*hooks, &block)
123
+ hooks << block if block
124
+ hooks.each { |hook| after_hooks.unshift(hook) }
125
+ end
126
+
127
+ # Internal: An Array of declared hooks to run around Interactor
128
+ # invocation. The hooks appear in the order in which they will be run.
129
+ #
130
+ # Examples
131
+ #
132
+ # class MyInteractor
133
+ # include Interactor
134
+ #
135
+ # around :time_execution, :use_transaction
136
+ # end
137
+ #
138
+ # MyInteractor.around_hooks
139
+ # # => [:time_execution, :use_transaction]
140
+ #
141
+ # Returns an Array of Symbols and Procs.
142
+ def around_hooks
143
+ @around_hooks ||= []
144
+ end
145
+
146
+ # Internal: An Array of declared hooks to run before Interactor
147
+ # invocation. The hooks appear in the order in which they will be run.
148
+ #
149
+ # Examples
150
+ #
151
+ # class MyInteractor
152
+ # include Interactor
153
+ #
154
+ # before :set_start_time, :say_hello
155
+ # end
156
+ #
157
+ # MyInteractor.before_hooks
158
+ # # => [:set_start_time, :say_hello]
159
+ #
160
+ # Returns an Array of Symbols and Procs.
161
+ def before_hooks
162
+ @before_hooks ||= []
163
+ end
164
+
165
+ # Internal: An Array of declared hooks to run before Interactor
166
+ # invocation. The hooks appear in the order in which they will be run.
167
+ #
168
+ # Examples
169
+ #
170
+ # class MyInteractor
171
+ # include Interactor
172
+ #
173
+ # after :set_finish_time, :say_goodbye
174
+ # end
175
+ #
176
+ # MyInteractor.after_hooks
177
+ # # => [:say_goodbye, :set_finish_time]
178
+ #
179
+ # Returns an Array of Symbols and Procs.
180
+ def after_hooks
181
+ @after_hooks ||= []
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ # Internal: Run around, before and after hooks around yielded execution. The
188
+ # required block is surrounded with hooks and executed.
189
+ #
190
+ # Examples
191
+ #
192
+ # class MyProcessor
193
+ # include Interactor::Hooks
194
+ #
195
+ # def process_with_hooks
196
+ # with_hooks do
197
+ # process
198
+ # end
199
+ # end
200
+ #
201
+ # def process
202
+ # puts "processed!"
203
+ # end
204
+ # end
205
+ #
206
+ # Returns nothing.
207
+ def with_hooks
208
+ run_around_hooks do
209
+ run_before_hooks
210
+ yield
211
+ run_after_hooks
212
+ end
213
+ end
214
+
215
+ # Internal: Run around hooks.
216
+ #
217
+ # Returns nothing.
218
+ def run_around_hooks(&block)
219
+ self.class.around_hooks.reverse.inject(block) { |chain, hook|
220
+ proc { run_hook(hook, chain) }
221
+ }.call
222
+ end
223
+
224
+ # Internal: Run before hooks.
225
+ #
226
+ # Returns nothing.
227
+ def run_before_hooks
228
+ run_hooks(self.class.before_hooks)
229
+ end
230
+
231
+ # Internal: Run after hooks.
232
+ #
233
+ # Returns nothing.
234
+ def run_after_hooks
235
+ run_hooks(self.class.after_hooks)
236
+ end
237
+
238
+ # Internal: Run a colection of hooks. The "run_hooks" method is the common
239
+ # interface by which collections of either before or after hooks are run.
240
+ #
241
+ # hooks - An Array of Symbol and Proc hooks.
242
+ #
243
+ # Returns nothing.
244
+ def run_hooks(hooks)
245
+ hooks.each { |hook| run_hook(hook) }
246
+ end
247
+
248
+ # Internal: Run an individual hook. The "run_hook" method is the common
249
+ # interface by which an individual hook is run. If the given hook is a
250
+ # symbol, the method is invoked whether public or private. If the hook is a
251
+ # proc, the proc is evaluated in the context of the current instance.
252
+ #
253
+ # hook - A Symbol or Proc hook.
254
+ # args - Zero or more arguments to be passed as block arguments into the
255
+ # given block or as arguments into the method described by the given
256
+ # Symbol method name.
257
+ #
258
+ # Returns nothing.
259
+ def run_hook(hook, *args)
260
+ hook.is_a?(Symbol) ? send(hook, *args) : instance_exec(*args, &hook)
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,69 @@
1
+ module Interactor
2
+ # Public: Interactor::Organizer methods. Because Interactor::Organizer is a
3
+ # module, custom Interactor::Organizer classes should include
4
+ # Interactor::Organizer rather than inherit from it.
5
+ #
6
+ # Examples
7
+ #
8
+ # class MyOrganizer
9
+ # include Interactor::Organizer
10
+ #
11
+ # organizer InteractorOne, InteractorTwo
12
+ # end
13
+ module Organizer
14
+ extend ActiveSupport::Concern
15
+ include Interactor
16
+
17
+ # Internal: Interactor::Organizer class methods.
18
+ class_methods do
19
+ # Public: Declare Interactors to be invoked as part of the
20
+ # Interactor::Organizer's invocation. These interactors are invoked in
21
+ # the order in which they are declared.
22
+ #
23
+ # interactors - Zero or more (or an Array of) Interactor classes.
24
+ #
25
+ # Examples
26
+ #
27
+ # class MyFirstOrganizer
28
+ # include Interactor::Organizer
29
+ #
30
+ # organize InteractorOne, InteractorTwo
31
+ # end
32
+ #
33
+ # class MySecondOrganizer
34
+ # include Interactor::Organizer
35
+ #
36
+ # organize [InteractorThree, InteractorFour]
37
+ # end
38
+ #
39
+ # Returns nothing.
40
+ def organize(*interactors)
41
+ @organized = interactors.flatten
42
+ end
43
+
44
+ # Internal: An Array of declared Interactors to be invoked.
45
+ #
46
+ # Examples
47
+ #
48
+ # class MyOrganizer
49
+ # include Interactor::Organizer
50
+ #
51
+ # organize InteractorOne, InteractorTwo
52
+ # end
53
+ #
54
+ # MyOrganizer.organized
55
+ # # => [InteractorOne, InteractorTwo]
56
+ #
57
+ # Returns an Array of Interactor classes or an empty Array.
58
+ def organized
59
+ @organized ||= []
60
+ end
61
+ end
62
+
63
+ def call
64
+ self.class.organized.inject(context) do |ctx, interactor|
65
+ interactor.call!(ctx)
66
+ end
67
+ end
68
+ end
69
+ end