interactor_with_steroids 0.0.1

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,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