interaktor 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +4 -0
- data/.rubocop.yml +313 -0
- data/.travis.yml +25 -0
- data/Gemfile +14 -0
- data/README.md +707 -0
- data/Rakefile +8 -0
- data/interaktor.gemspec +19 -0
- data/lib/interaktor.rb +209 -0
- data/lib/interaktor/context.rb +91 -0
- data/lib/interaktor/failure.rb +13 -0
- data/lib/interaktor/hooks.rb +264 -0
- data/lib/interaktor/organizer.rb +67 -0
- data/spec/integration_spec.rb +1786 -0
- data/spec/interactor/context_spec.rb +187 -0
- data/spec/interactor/hooks_spec.rb +358 -0
- data/spec/interactor/organizer_spec.rb +61 -0
- data/spec/interactor_spec.rb +3 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/lint.rb +136 -0
- metadata +97 -0
data/Rakefile
ADDED
data/interaktor.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = "interaktor"
|
3
|
+
spec.version = "0.1.3"
|
4
|
+
|
5
|
+
spec.author = "Taylor Thurlow"
|
6
|
+
spec.email = "taylorthurlow@me.com"
|
7
|
+
spec.description = "A common interface for building service objects."
|
8
|
+
spec.summary = "Simple service object implementation"
|
9
|
+
spec.homepage = "https://github.com/taylorthurlow/interaktor"
|
10
|
+
spec.license = "MIT"
|
11
|
+
spec.files = `git ls-files`.split
|
12
|
+
spec.test_files = spec.files.grep(/^spec/)
|
13
|
+
spec.required_ruby_version = ">= 2.5"
|
14
|
+
spec.require_path = "lib"
|
15
|
+
|
16
|
+
spec.add_runtime_dependency "zeitwerk", "~> 2.3.1"
|
17
|
+
|
18
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
19
|
+
end
|
data/lib/interaktor.rb
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
require "zeitwerk"
|
2
|
+
|
3
|
+
loader = Zeitwerk::Loader.for_gem
|
4
|
+
loader.push_dir(File.expand_path("../lib", __dir__))
|
5
|
+
loader.setup
|
6
|
+
|
7
|
+
module Interaktor
|
8
|
+
# When the Interaktor module is included in a class, add the relevant class
|
9
|
+
# methods and hooks to that class.
|
10
|
+
#
|
11
|
+
# @param base [Class] the class which is including the Interaktor module
|
12
|
+
def self.included(base)
|
13
|
+
base.class_eval do
|
14
|
+
extend ClassMethods
|
15
|
+
include Hooks
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Interaktor::Context] this should not be used publicly
|
19
|
+
attr_accessor :context
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
# The list of attributes which are required to be passed in when calling
|
24
|
+
# the interaktor.
|
25
|
+
#
|
26
|
+
# @return [Array<Symbol>]
|
27
|
+
def required_attributes
|
28
|
+
@required_attributes ||= []
|
29
|
+
end
|
30
|
+
|
31
|
+
# The list of attributes which are NOT required to be passed in when
|
32
|
+
# calling the interaktor.
|
33
|
+
#
|
34
|
+
# @return [Array<Symbol>]
|
35
|
+
def optional_attributes
|
36
|
+
@optional_attributes ||= []
|
37
|
+
end
|
38
|
+
|
39
|
+
# The list of attributes which are required to be passed in when calling
|
40
|
+
# `#fail!` from within the interaktor.
|
41
|
+
#
|
42
|
+
# @return [Array<Symbol>]
|
43
|
+
def failure_attributes
|
44
|
+
@failure_attributes ||= []
|
45
|
+
end
|
46
|
+
|
47
|
+
# A DSL method for documenting required interaktor attributes.
|
48
|
+
#
|
49
|
+
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
50
|
+
#
|
51
|
+
# @return [void]
|
52
|
+
def required(*attributes)
|
53
|
+
required_attributes.concat attributes
|
54
|
+
|
55
|
+
attributes.each do |attribute|
|
56
|
+
define_method(attribute) { context.send(attribute) }
|
57
|
+
define_method("#{attribute}=".to_sym) do |value|
|
58
|
+
context.send("#{attribute}=".to_sym, value)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# A DSL method for documenting optional interaktor attributes.
|
64
|
+
#
|
65
|
+
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
66
|
+
#
|
67
|
+
# @return [void]
|
68
|
+
def optional(*attributes)
|
69
|
+
optional_attributes.concat attributes
|
70
|
+
|
71
|
+
attributes.each do |attribute|
|
72
|
+
define_method(attribute) { context.send(attribute) }
|
73
|
+
define_method("#{attribute}=".to_sym) do |value|
|
74
|
+
unless context.to_h.key?(attribute)
|
75
|
+
raise <<~ERROR
|
76
|
+
You can't assign a value to an optional parameter if you didn't
|
77
|
+
initialize the interaktor with it in the first place.
|
78
|
+
ERROR
|
79
|
+
end
|
80
|
+
|
81
|
+
context.send("#{attribute}=".to_sym, value)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# A DSL method for documenting required interaktor failure attributes.
|
87
|
+
#
|
88
|
+
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
89
|
+
#
|
90
|
+
# @return [void]
|
91
|
+
def failure(*attributes)
|
92
|
+
failure_attributes.concat attributes
|
93
|
+
end
|
94
|
+
|
95
|
+
# Invoke an Interaktor. This is the primary public API method to an
|
96
|
+
# interaktor.
|
97
|
+
#
|
98
|
+
# @param context [Hash, Interaktor::Context] the context object as a hash
|
99
|
+
# with attributes or an already-built context
|
100
|
+
#
|
101
|
+
# @return [Interaktor::Context] the context, following interaktor execution
|
102
|
+
def call(context = {})
|
103
|
+
verify_attribute_presence(context)
|
104
|
+
|
105
|
+
new(context).tap(&:run).context
|
106
|
+
end
|
107
|
+
|
108
|
+
# Invoke an Interaktor. This method behaves identically to `#call`, with
|
109
|
+
# one notable exception - if the context is failed during the invocation of
|
110
|
+
# the interaktor, `Interaktor::Failure` is raised.
|
111
|
+
#
|
112
|
+
# @param context [Hash, Interaktor::Context] the context object as a hash
|
113
|
+
# with attributes or an already-built context
|
114
|
+
#
|
115
|
+
# @raises [Interaktor::Failure]
|
116
|
+
#
|
117
|
+
# @return [Interaktor::Context] the context, following interaktor execution
|
118
|
+
def call!(context = {})
|
119
|
+
verify_attribute_presence(context)
|
120
|
+
|
121
|
+
new(context).tap(&:run!).context
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
# Check the provided context against the attributes defined with the DSL
|
127
|
+
# methods, and determine if there are any attributes which are required and
|
128
|
+
# have not been provided.
|
129
|
+
#
|
130
|
+
# @param context [Interaktor::Context] the context to check
|
131
|
+
#
|
132
|
+
# @return [void]
|
133
|
+
def verify_attribute_presence(context)
|
134
|
+
# TODO: Add "allow_nil?" option to required attributes
|
135
|
+
missing_attrs = required_attributes.reject { |required_attr| context.to_h.key?(required_attr) }
|
136
|
+
|
137
|
+
raise <<~ERROR if missing_attrs.any?
|
138
|
+
Required attribute(s) were not provided when initializing #{name} interaktor:
|
139
|
+
#{missing_attrs.join("\n ")}
|
140
|
+
ERROR
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# @param context [Hash, Interaktor::Context] the context object as a hash
|
145
|
+
# with attributes or an already-built context
|
146
|
+
def initialize(context = {})
|
147
|
+
@context = Interaktor::Context.build(context)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Fail the current interaktor.
|
151
|
+
#
|
152
|
+
# @param failure_attributes [Hash{Symbol=>Object}] the context attributes
|
153
|
+
#
|
154
|
+
# @return [void]
|
155
|
+
def fail!(failure_attributes = {})
|
156
|
+
# Make sure we have all required attributes
|
157
|
+
missing_attrs = self.class.failure_attributes
|
158
|
+
.reject { |failure_attr| failure_attributes.key?(failure_attr) }
|
159
|
+
raise "Missing failure attrs: #{missing_attrs.join(", ")}" if missing_attrs.any?
|
160
|
+
|
161
|
+
context.fail!(failure_attributes)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Invoke an Interaktor instance without any hooks, tracking, or rollback. It
|
165
|
+
# is expected that the `#call` instance method is overwritten for each
|
166
|
+
# interaktor class.
|
167
|
+
#
|
168
|
+
# @return [void]
|
169
|
+
def call; end
|
170
|
+
|
171
|
+
# Reverse prior invocation of an Interaktor instance. Any interaktor class
|
172
|
+
# that requires undoing upon downstream failure is expected to overwrite the
|
173
|
+
# `#rollback` instance method.
|
174
|
+
#
|
175
|
+
# @return [void]
|
176
|
+
def rollback; end
|
177
|
+
|
178
|
+
# Invoke an interaktor instance along with all defined hooks. The `run`
|
179
|
+
# method is used internally by the `call` class method. After successful
|
180
|
+
# invocation of the interaktor, the instance is tracked within the context.
|
181
|
+
# If the context is failed or any error is raised, the context is rolled
|
182
|
+
# back.
|
183
|
+
#
|
184
|
+
# @return [void]
|
185
|
+
def run
|
186
|
+
run!
|
187
|
+
rescue Interaktor::Failure # rubocop:disable Lint/SuppressedException
|
188
|
+
end
|
189
|
+
|
190
|
+
# Invoke an Interaktor instance along with all defined hooks, typically used
|
191
|
+
# internally by `.call!`. After successful invocation of the interaktor, the
|
192
|
+
# instance is tracked within the context. If the context is failed or any
|
193
|
+
# error is raised, the context is rolled back. This method behaves
|
194
|
+
# identically to `#run` with one notable exception - if the context is failed
|
195
|
+
# during the invocation of the interaktor, `Interaktor::Failure` is raised.
|
196
|
+
#
|
197
|
+
# @raises [Interaktor::Failure]
|
198
|
+
#
|
199
|
+
# @return [void]
|
200
|
+
def run!
|
201
|
+
with_hooks do
|
202
|
+
call
|
203
|
+
context.called!(self)
|
204
|
+
end
|
205
|
+
rescue StandardError
|
206
|
+
context.rollback!
|
207
|
+
raise
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
|
3
|
+
# The object for tracking state of an Interaktor's invocation. The context is
|
4
|
+
# used to initialize the interaktor with the information required for
|
5
|
+
# invocation. The interaktor manipulates the context to produce the result of
|
6
|
+
# invocation. The context is the mechanism by which success and failure are
|
7
|
+
# determined and the context is responsible for tracking individual interaktor
|
8
|
+
# invocations for the purpose of rollback. It may be manipulated using
|
9
|
+
# arbitrary getter and setter methods.
|
10
|
+
class Interaktor::Context < OpenStruct
|
11
|
+
# Initialize an Interaktor::Context or preserve an existing one. If the
|
12
|
+
# argument given is an Interaktor::Context, the argument is returned.
|
13
|
+
# Otherwise, a new Interaktor::Context is initialized from the provided hash.
|
14
|
+
# Used during interaktor initialization.
|
15
|
+
#
|
16
|
+
# @param context [Hash, Interaktor::Context] the context object as a hash
|
17
|
+
# with attributes or an already-built context
|
18
|
+
#
|
19
|
+
# @return [Interaktor::Context]
|
20
|
+
def self.build(context = {})
|
21
|
+
context.is_a?(Interaktor::Context) ? context : new(context)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Whether the Interaktor::Context is successful. By default, a new context is
|
25
|
+
# successful and only changes when explicitly failed. This method is the
|
26
|
+
# inverse of the `#failure?` method.
|
27
|
+
#
|
28
|
+
# @return [Boolean] true by default, or false if failed
|
29
|
+
def success?
|
30
|
+
!failure?
|
31
|
+
end
|
32
|
+
|
33
|
+
# Whether the Interaktor::Context has failed. By default, a new context is
|
34
|
+
# successful and only changes when explicitly failed. This method is the
|
35
|
+
# inverse of the `#success?` method.
|
36
|
+
#
|
37
|
+
# @return [Boolean] false by default, or true if failed
|
38
|
+
def failure?
|
39
|
+
@failure || false
|
40
|
+
end
|
41
|
+
|
42
|
+
# Fail the Interaktor::Context. Failing a context raises an error that may be
|
43
|
+
# rescued by the calling interaktor. The context is also flagged as having
|
44
|
+
# failed. Optionally the caller may provide a hash of key/value pairs to be
|
45
|
+
# merged into the context before failure.
|
46
|
+
#
|
47
|
+
# @param context [Hash] data to be merged into the existing context
|
48
|
+
#
|
49
|
+
# @raises [Interaktor::Failure]
|
50
|
+
#
|
51
|
+
# @return [void]
|
52
|
+
def fail!(context = {})
|
53
|
+
context.each { |key, value| self[key.to_sym] = value }
|
54
|
+
@failure = true
|
55
|
+
raise Interaktor::Failure, self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Roll back the Interaktor::Context. Any interaktors to which this context
|
59
|
+
# has been passed and which have been successfully called are asked to roll
|
60
|
+
# themselves back by invoking their `#rollback` methods.
|
61
|
+
#
|
62
|
+
# @return [Boolean] true if rolled back successfully, false if already
|
63
|
+
# rolled back
|
64
|
+
def rollback!
|
65
|
+
return false if @rolled_back
|
66
|
+
|
67
|
+
_called.reverse_each(&:rollback)
|
68
|
+
@rolled_back = true
|
69
|
+
end
|
70
|
+
|
71
|
+
# Track that an Interaktor has been called. The `#called!` method is used by
|
72
|
+
# the interaktor being invoked with this context. After an interaktor is
|
73
|
+
# successfully called, the interaktor instance is tracked in the context for
|
74
|
+
# the purpose of potential future rollback.
|
75
|
+
#
|
76
|
+
# @param interaktor [Interaktor] an interaktor that has been successfully
|
77
|
+
# called
|
78
|
+
#
|
79
|
+
# @return [void]
|
80
|
+
def called!(interaktor)
|
81
|
+
_called << interaktor
|
82
|
+
end
|
83
|
+
|
84
|
+
# An array of successfully called Interaktor instances invoked against this
|
85
|
+
# Interaktor::Context instance.
|
86
|
+
#
|
87
|
+
# @return [Array<Interaktor>]
|
88
|
+
def _called
|
89
|
+
@called ||= []
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Error raised during Interaktor::Context failure. The error stores a copy of
|
2
|
+
# the failed context for debugging purposes.
|
3
|
+
class Interaktor::Failure < StandardError
|
4
|
+
# @return [Interaktor::Context] the context of this failure instance
|
5
|
+
attr_reader :context
|
6
|
+
|
7
|
+
# @param context [Interaktor::Context] the context in which the error was
|
8
|
+
# raised
|
9
|
+
def initialize(context = nil)
|
10
|
+
@context = context
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,264 @@
|
|
1
|
+
# Internal: Methods relating to supporting hooks around Interaktor invocation.
|
2
|
+
module Interaktor::Hooks
|
3
|
+
# Internal: Install Interaktor's behavior in the given class.
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
extend ClassMethods
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# Internal: Interaktor::Hooks class methods.
|
11
|
+
module ClassMethods
|
12
|
+
# Public: Declare hooks to run around Interaktor invocation. The around
|
13
|
+
# method may be called multiple times; subsequent calls append declared
|
14
|
+
# hooks to existing around hooks.
|
15
|
+
#
|
16
|
+
# hooks - Zero or more Symbol method names representing instance methods
|
17
|
+
# to be called around interaktor invocation. Each instance method
|
18
|
+
# invocation receives an argument representing the next link in
|
19
|
+
# the around hook chain.
|
20
|
+
# block - An optional block to be executed as a hook. If given, the block
|
21
|
+
# is executed after methods corresponding to any given Symbols.
|
22
|
+
#
|
23
|
+
# Examples
|
24
|
+
#
|
25
|
+
# class MyInteraktor
|
26
|
+
# include Interaktor
|
27
|
+
#
|
28
|
+
# around :time_execution
|
29
|
+
#
|
30
|
+
# around do |interaktor|
|
31
|
+
# puts "started"
|
32
|
+
# interaktor.call
|
33
|
+
# puts "finished"
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# def call
|
37
|
+
# puts "called"
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# private
|
41
|
+
#
|
42
|
+
# def time_execution(interaktor)
|
43
|
+
# context.start_time = Time.now
|
44
|
+
# interaktor.call
|
45
|
+
# context.finish_time = Time.now
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# Returns nothing.
|
50
|
+
def around(*hooks, &block)
|
51
|
+
hooks << block if block
|
52
|
+
hooks.each { |hook| around_hooks.push(hook) }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Public: Declare hooks to run before Interaktor invocation. The before
|
56
|
+
# method may be called multiple times; subsequent calls append declared
|
57
|
+
# hooks to existing before hooks.
|
58
|
+
#
|
59
|
+
# hooks - Zero or more Symbol method names representing instance methods
|
60
|
+
# to be called before interaktor invocation.
|
61
|
+
# block - An optional block to be executed as a hook. If given, the block
|
62
|
+
# is executed after methods corresponding to any given Symbols.
|
63
|
+
#
|
64
|
+
# Examples
|
65
|
+
#
|
66
|
+
# class MyInteraktor
|
67
|
+
# include Interaktor
|
68
|
+
#
|
69
|
+
# before :set_start_time
|
70
|
+
#
|
71
|
+
# before do
|
72
|
+
# puts "started"
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# def call
|
76
|
+
# puts "called"
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# private
|
80
|
+
#
|
81
|
+
# def set_start_time
|
82
|
+
# context.start_time = Time.now
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# Returns nothing.
|
87
|
+
def before(*hooks, &block)
|
88
|
+
hooks << block if block
|
89
|
+
hooks.each { |hook| before_hooks.push(hook) }
|
90
|
+
end
|
91
|
+
|
92
|
+
# Public: Declare hooks to run after Interaktor invocation. The after
|
93
|
+
# method may be called multiple times; subsequent calls prepend declared
|
94
|
+
# hooks to existing after hooks.
|
95
|
+
#
|
96
|
+
# hooks - Zero or more Symbol method names representing instance methods
|
97
|
+
# to be called after interaktor invocation.
|
98
|
+
# block - An optional block to be executed as a hook. If given, the block
|
99
|
+
# is executed before methods corresponding to any given Symbols.
|
100
|
+
#
|
101
|
+
# Examples
|
102
|
+
#
|
103
|
+
# class MyInteraktor
|
104
|
+
# include Interaktor
|
105
|
+
#
|
106
|
+
# after :set_finish_time
|
107
|
+
#
|
108
|
+
# after do
|
109
|
+
# puts "finished"
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# def call
|
113
|
+
# puts "called"
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# private
|
117
|
+
#
|
118
|
+
# def set_finish_time
|
119
|
+
# context.finish_time = Time.now
|
120
|
+
# end
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# Returns nothing.
|
124
|
+
def after(*hooks, &block)
|
125
|
+
hooks << block if block
|
126
|
+
hooks.each { |hook| after_hooks.unshift(hook) }
|
127
|
+
end
|
128
|
+
|
129
|
+
# Internal: An Array of declared hooks to run around Interaktor
|
130
|
+
# invocation. The hooks appear in the order in which they will be run.
|
131
|
+
#
|
132
|
+
# Examples
|
133
|
+
#
|
134
|
+
# class MyInteraktor
|
135
|
+
# include Interaktor
|
136
|
+
#
|
137
|
+
# around :time_execution, :use_transaction
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# MyInteraktor.around_hooks
|
141
|
+
# # => [:time_execution, :use_transaction]
|
142
|
+
#
|
143
|
+
# Returns an Array of Symbols and Procs.
|
144
|
+
def around_hooks
|
145
|
+
@around_hooks ||= []
|
146
|
+
end
|
147
|
+
|
148
|
+
# Internal: An Array of declared hooks to run before Interaktor
|
149
|
+
# invocation. The hooks appear in the order in which they will be run.
|
150
|
+
#
|
151
|
+
# Examples
|
152
|
+
#
|
153
|
+
# class MyInteraktor
|
154
|
+
# include Interaktor
|
155
|
+
#
|
156
|
+
# before :set_start_time, :say_hello
|
157
|
+
# end
|
158
|
+
#
|
159
|
+
# MyInteraktor.before_hooks
|
160
|
+
# # => [:set_start_time, :say_hello]
|
161
|
+
#
|
162
|
+
# Returns an Array of Symbols and Procs.
|
163
|
+
def before_hooks
|
164
|
+
@before_hooks ||= []
|
165
|
+
end
|
166
|
+
|
167
|
+
# Internal: An Array of declared hooks to run before Interaktor
|
168
|
+
# invocation. The hooks appear in the order in which they will be run.
|
169
|
+
#
|
170
|
+
# Examples
|
171
|
+
#
|
172
|
+
# class MyInteraktor
|
173
|
+
# include Interaktor
|
174
|
+
#
|
175
|
+
# after :set_finish_time, :say_goodbye
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# MyInteraktor.after_hooks
|
179
|
+
# # => [:say_goodbye, :set_finish_time]
|
180
|
+
#
|
181
|
+
# Returns an Array of Symbols and Procs.
|
182
|
+
def after_hooks
|
183
|
+
@after_hooks ||= []
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
# Internal: Run around, before and after hooks around yielded execution. The
|
190
|
+
# required block is surrounded with hooks and executed.
|
191
|
+
#
|
192
|
+
# Examples
|
193
|
+
#
|
194
|
+
# class MyProcessor
|
195
|
+
# include Interaktor::Hooks
|
196
|
+
#
|
197
|
+
# def process_with_hooks
|
198
|
+
# with_hooks do
|
199
|
+
# process
|
200
|
+
# end
|
201
|
+
# end
|
202
|
+
#
|
203
|
+
# def process
|
204
|
+
# puts "processed!"
|
205
|
+
# end
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
# Returns nothing.
|
209
|
+
def with_hooks
|
210
|
+
run_around_hooks do
|
211
|
+
run_before_hooks
|
212
|
+
yield
|
213
|
+
run_after_hooks
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Internal: Run around hooks.
|
218
|
+
#
|
219
|
+
# Returns nothing.
|
220
|
+
def run_around_hooks(&block)
|
221
|
+
self.class.around_hooks.reverse.inject(block) { |chain, hook|
|
222
|
+
proc { run_hook(hook, chain) }
|
223
|
+
}.call
|
224
|
+
end
|
225
|
+
|
226
|
+
# Internal: Run before hooks.
|
227
|
+
#
|
228
|
+
# Returns nothing.
|
229
|
+
def run_before_hooks
|
230
|
+
run_hooks(self.class.before_hooks)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Internal: Run after hooks.
|
234
|
+
#
|
235
|
+
# Returns nothing.
|
236
|
+
def run_after_hooks
|
237
|
+
run_hooks(self.class.after_hooks)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Internal: Run a colection of hooks. The "run_hooks" method is the common
|
241
|
+
# interface by which collections of either before or after hooks are run.
|
242
|
+
#
|
243
|
+
# hooks - An Array of Symbol and Proc hooks.
|
244
|
+
#
|
245
|
+
# Returns nothing.
|
246
|
+
def run_hooks(hooks)
|
247
|
+
hooks.each { |hook| run_hook(hook) }
|
248
|
+
end
|
249
|
+
|
250
|
+
# Internal: Run an individual hook. The "run_hook" method is the common
|
251
|
+
# interface by which an individual hook is run. If the given hook is a
|
252
|
+
# symbol, the method is invoked whether public or private. If the hook is a
|
253
|
+
# proc, the proc is evaluated in the context of the current instance.
|
254
|
+
#
|
255
|
+
# hook - A Symbol or Proc hook.
|
256
|
+
# args - Zero or more arguments to be passed as block arguments into the
|
257
|
+
# given block or as arguments into the method described by the given
|
258
|
+
# Symbol method name.
|
259
|
+
#
|
260
|
+
# Returns nothing.
|
261
|
+
def run_hook(hook, *args)
|
262
|
+
hook.is_a?(Symbol) ? send(hook, *args) : instance_exec(*args, &hook)
|
263
|
+
end
|
264
|
+
end
|