forked from mongodb/mongoid
/
interceptable.rb
282 lines (263 loc) · 8.61 KB
/
interceptable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# encoding: utf-8
module Mongoid
# This module contains all the callback hooks for Mongoid.
#
# @since 4.0.0
module Interceptable
extend ActiveSupport::Concern
CALLBACKS = [
:after_build,
:after_create,
:after_destroy,
:after_find,
:after_initialize,
:after_save,
:after_touch,
:after_update,
:after_upsert,
:after_validation,
:around_create,
:around_destroy,
:around_save,
:around_update,
:around_upsert,
:before_create,
:before_destroy,
:before_save,
:before_update,
:before_upsert,
:before_validation
].freeze
included do
extend ActiveModel::Callbacks
include ActiveModel::Validations::Callbacks
define_model_callbacks :build, :find, :initialize, :touch, only: :after
define_model_callbacks :create, :destroy, :save, :update, :upsert
attr_accessor :before_callback_halted
end
# Is the provided type of callback executable by this document?
#
# @example Is the callback executable?
# document.callback_executable?(:save)
#
# @param [ Symbol ] kin The type of callback.
#
# @return [ true, false ] If the callback can be executed.
#
# @since 3.0.6
def callback_executable?(kind)
respond_to?("_#{kind}_callbacks")
end
# Is the document currently in a state that could potentially require
# callbacks to be executed?
#
# @example Is the document in a callback state?
# document.in_callback_state?(:update)
#
# @param [ Symbol ] kind The callback kind.
#
# @return [ true, false ] If the document is in a callback state.
#
# @since 3.1.0
def in_callback_state?(kind)
[ :create, :destroy ].include?(kind) || new_record? || flagged_for_destroy? || changed?
end
# Run only the after callbacks for the specific event.
#
# @note ActiveSupport does not allow this type of behaviour by default, so
# Mongoid has to get around it and implement itself.
#
# @example Run only the after save callbacks.
# model.run_after_callbacks(:save)
#
# @param [ Array<Symbol> ] kinds The events that are occurring.
#
# @return [ Object ] The result of the chain executing.
#
# @since 3.0.0
def run_after_callbacks(*kinds)
kinds.each do |kind|
run_targeted_callbacks(:after, kind)
end
end
# Run only the before callbacks for the specific event.
#
# @note ActiveSupport does not allow this type of behaviour by default, so
# Mongoid has to get around it and implement itself.
#
# @example Run only the before save callbacks.
# model.run_before_callbacks(:save, :create)
#
# @param [ Array<Symbol> ] kinds The events that are occurring.
#
# @return [ Object ] The result of the chain executing.
#
# @since 3.0.0
def run_before_callbacks(*kinds)
kinds.each do |kind|
run_targeted_callbacks(:before, kind)
end
end
# Run the callbacks for the document. This overrides active support's
# functionality to cascade callbacks to embedded documents that have been
# flagged as such.
#
# @example Run the callbacks.
# run_callbacks :save do
# save!
# end
#
# @param [ Symbol ] kind The type of callback to execute.
# @param [ Array ] *args Any options.
#
# @return [ Document ] The document
#
# @since 2.3.0
def run_callbacks(kind, *args, &block)
cascadable_children(kind).each do |child|
# This is returning false for some destroy tests on 4.1.0.beta1,
# causing them to fail since 4.1.0 expects a block to be passed if the
# callbacks for the type are empty. If no block is passed then the nil
# return value gets interpreted as false and halts the chain.
#
# @see https://github.com/rails/rails/blob/master/activesupport/lib/active_support/callbacks.rb#L79
if child.run_callbacks(child_callback_type(kind, child), *args) == false
return false
end
end
callback_executable?(kind) ? super(kind, *args, &block) : true
end
private
# We need to hook into this for autosave, since we don't want it firing if
# the before callbacks were halted.
#
# @api private
#
# @example Was a before callback halted?
# document.before_callback_halted?
#
# @return [ true, false ] If a before callback was halted.
#
# @since 3.0.3
def before_callback_halted?
!!@before_callback_halted
end
# Get all the child embedded documents that are flagged as cascadable.
#
# @example Get all the cascading children.
# document.cascadable_children(:update)
#
# @param [ Symbol ] kind The type of callback.
#
# @return [ Array<Document> ] The children.
#
# @since 2.3.0
def cascadable_children(kind, children = Set.new)
embedded_relations.each_pair do |name, metadata|
next unless metadata.cascading_callbacks?
without_autobuild do
delayed_pulls = delayed_atomic_pulls[name]
delayed_unsets = delayed_atomic_unsets[name]
children.merge(delayed_pulls) if delayed_pulls
children.merge(delayed_unsets) if delayed_unsets
relation = send(name)
Array.wrap(relation).each do |child|
next if children.include?(child)
children.add(child) if cascadable_child?(kind, child, metadata)
child.send(:cascadable_children, kind, children)
end
end
end
children.to_a
end
# Determine if the child should fire the callback.
#
# @example Should the child fire the callback?
# document.cascadable_child?(:update, doc)
#
# @param [ Symbol ] kind The type of callback.
# @param [ Document ] child The child document.
#
# @return [ true, false ] If the child should fire the callback.
#
# @since 2.3.0
def cascadable_child?(kind, child, metadata)
return false if kind == :initialize || kind == :find
return false if kind == :validate && metadata.validate?
child.callback_executable?(kind) ? child.in_callback_state?(kind) : false
end
# Get the name of the callback that the child should fire. This changes
# depending on whether or not the child is new. A persisted parent with a
# new child would fire :update from the parent, but needs to fire :create
# on the child.
#
# @example Get the callback type.
# document.child_callback_type(:update, doc)
#
# @param [ Symbol ] kind The type of callback.
# @param [ Document ] child The child document
#
# @return [ Symbol ] The name of the callback.
#
# @since 2.3.0
def child_callback_type(kind, child)
if kind == :update
return :create if child.new_record?
return :destroy if child.flagged_for_destroy?
kind
else
kind
end
end
# We need to hook into this for autosave, since we don't want it firing if
# the before callbacks were halted.
#
# @api private
#
# @example Hook into the halt.
# document.halted_callback_hook(filter)
#
# @param [ Symbol ] filter The callback that halted.
#
# @since 3.0.3
def halted_callback_hook(filter)
@before_callback_halted = true
end
# Run only the callbacks for the target location (before, after, around)
# and kind (save, update, create).
#
# @example Run the targeted callbacks.
# model.run_targeted_callbacks(:before, :save)
#
# @param [ Symbol ] place The time to run, :before, :after, :around.
# @param [ Symbol ] kind The type of callback, :save, :create, :update.
#
# @return [ Object ] The result of the chain execution.
#
# @since 3.0.0
def run_targeted_callbacks(place, kind)
name = "_run__#{place}__#{kind}__callbacks"
unless respond_to?(name)
chain = ActiveSupport::Callbacks::CallbackChain.new(name, {})
send("_#{kind}_callbacks").each do |callback|
chain.append(callback) if callback.kind == place
end
if Gem::Version.new("4.1.0") <= Gem::Version.new(ActiveSupport::VERSION::STRING)
self.class.send :define_method, name do
runner = ActiveSupport::Callbacks::Filters::Environment.new(self, false, nil)
chain.compile.call(runner).value
end
self.class.send :protected, name
else
class_eval <<-EOM
def #{name}()
#{chain.compile}
end
protected :#{name}
EOM
end
end
send(name)
end
end
end