interaktor 0.1.3
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.
- 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
|