siderite_orm/signals.rs
1//! Model lifecycle signals: `pre_save`, `post_save`, `pre_delete`,
2//! `post_delete` and `m2m_changed` (Django `django.db.models.signals`).
3//!
4//! A [`Signals`] registry lives on the [`Db`] (see [`Db::with_signals`]);
5//! clones of the handle and the transaction handles derived from it share the
6//! same registry. Registration is explicit:
7//!
8//! ```ignore
9//! #[receiver(post_save, model = User)]
10//! async fn audit(user: &User, event: &SignalEvent<'_>) -> Result<(), SignalError> {
11//! // `event.db` is the handle of the operation, so this runs inside the
12//! // caller's transaction when there is one.
13//! Ok(())
14//! }
15//!
16//! let signals = Signals::new();
17//! signals.connect(audit_receiver());
18//! let db = Db::new(backend).with_signals(signals);
19//! ```
20//!
21//! # Semantics
22//!
23//! * Receivers are typed per model and awaited one after the other, in the
24//! order they were connected.
25//! * They receive the same [`Db`] as the operation, so inside a transaction
26//! they run in it.
27//! * A failing `pre_*` receiver aborts the operation (nothing is written) and
28//! the error surfaces as [`OrmError::Signal`]. Receivers connected after
29//! the failing one do not run.
30//! * A failing `post_*` receiver returns its error after the statement ran;
31//! inside a transaction the caller's rollback undoes the statement. Use
32//! [`Db::on_commit`] for work that must only happen after a commit.
33//! * `post_delete` fires only when a row was actually removed.
34//! * Bulk [`QuerySet`](crate::QuerySet) `update` / `delete` send no signals.
35//! * `m2m_changed` fires for `add`, `remove`, `clear` and `set` (which is a
36//! `remove` of the stale links followed by an `add` of the missing ones),
37//! with the [`M2mAction`] `Pre*` before and `Post*` after the join-table
38//! statements; `add` and `remove` with nothing to change send nothing. The
39//! receiver is typed on the *declaring* model and gets the source object,
40//! loaded by primary key only when such a receiver is connected.
41
42use crate::db::Db;
43use crate::error::OrmError;
44use crate::model::{Model, ModelMeta};
45use crate::value::Value;
46use std::any::{Any, TypeId};
47use std::error::Error;
48use std::future::Future;
49use std::pin::Pin;
50use std::sync::{Arc, PoisonError, RwLock, RwLockReadGuard};
51
52/// Boxed, sendable future returned by receivers.
53pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
54
55/// The signal a [`Receiver`] listens to, without event payload.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum SignalName {
58 /// Before an instance is inserted or updated.
59 PreSave,
60 /// After an instance was inserted or updated.
61 PostSave,
62 /// Before an instance is deleted.
63 PreDelete,
64 /// After an instance was deleted.
65 PostDelete,
66 /// Around a many-to-many change (all [`M2mAction`]s).
67 M2mChanged,
68}
69
70/// What happened, as delivered to a receiver in [`SignalEvent::kind`].
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum SignalKind {
73 /// Before `save`.
74 PreSave,
75 /// After `save`; `created` tells an INSERT from an UPDATE.
76 PostSave {
77 /// `true` if the row was inserted, `false` if it was updated.
78 created: bool,
79 },
80 /// Before `delete`.
81 PreDelete,
82 /// After `delete` removed a row.
83 PostDelete,
84 /// Around a many-to-many mutation.
85 M2mChanged {
86 /// Which mutation, and whether it is before or after.
87 action: M2mAction,
88 },
89}
90
91impl SignalKind {
92 /// The [`SignalName`] this event belongs to.
93 pub fn name(&self) -> SignalName {
94 match self {
95 Self::PreSave => SignalName::PreSave,
96 Self::PostSave { .. } => SignalName::PostSave,
97 Self::PreDelete => SignalName::PreDelete,
98 Self::PostDelete => SignalName::PostDelete,
99 Self::M2mChanged { .. } => SignalName::M2mChanged,
100 }
101 }
102}
103
104/// The step of a many-to-many mutation an `m2m_changed` event reports.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106pub enum M2mAction {
107 /// Before links are added.
108 PreAdd,
109 /// After links were added.
110 PostAdd,
111 /// Before links are removed.
112 PreRemove,
113 /// After links were removed.
114 PostRemove,
115 /// Before all links are removed.
116 PreClear,
117 /// After all links were removed.
118 PostClear,
119}
120
121/// Context handed to a receiver next to the instance.
122#[derive(Debug)]
123#[non_exhaustive]
124pub struct SignalEvent<'a> {
125 /// Which signal fired, with its payload.
126 pub kind: SignalKind,
127 /// The handle the operation runs on (the open transaction, if any).
128 pub db: &'a Db,
129 /// Metadata of the instance's model.
130 pub model: &'static ModelMeta,
131 /// `m2m_changed` only: the relation's field name on the declaring model.
132 pub relation: Option<&'static str>,
133 /// `m2m_changed` only: primary keys of the targets involved (empty for
134 /// `clear`).
135 pub pk_set: &'a [Value],
136}
137
138/// Error a receiver returns to fail the operation.
139///
140/// Build one from a message ([`SignalError::new`]) or from any error
141/// ([`SignalError::from_error`], or `?` on an [`OrmError`]). The text is
142/// logged and never returned to HTTP clients.
143#[derive(Debug, thiserror::Error)]
144#[error("signal receiver failed: {message}")]
145pub struct SignalError {
146 message: String,
147 #[source]
148 source: Option<Box<dyn Error + Send + Sync>>,
149}
150
151impl SignalError {
152 /// A failure described by `message`.
153 pub fn new(message: impl Into<String>) -> Self {
154 Self {
155 message: message.into(),
156 source: None,
157 }
158 }
159
160 /// A failure caused by `error`, which is kept as the source.
161 pub fn from_error(error: impl Error + Send + Sync + 'static) -> Self {
162 Self {
163 message: error.to_string(),
164 source: Some(Box::new(error)),
165 }
166 }
167
168 /// The failure message.
169 pub fn message(&self) -> &str {
170 &self.message
171 }
172}
173
174impl From<OrmError> for SignalError {
175 fn from(error: OrmError) -> Self {
176 Self::from_error(error)
177 }
178}
179
180/// Type-erased receiver body.
181type Handler = dyn for<'a> Fn(
182 &'a (dyn Any + Send + Sync),
183 &'a SignalEvent<'a>,
184 ) -> BoxFuture<'a, Result<(), SignalError>>
185 + Send
186 + Sync;
187
188/// Pins the higher-ranked signature so closures infer it.
189fn erase<F>(f: F) -> F
190where
191 F: for<'a> Fn(
192 &'a (dyn Any + Send + Sync),
193 &'a SignalEvent<'a>,
194 ) -> BoxFuture<'a, Result<(), SignalError>>
195 + Send
196 + Sync,
197{
198 f
199}
200
201/// One async callback for one signal of one model.
202///
203/// Usually produced by `#[receiver(..)]`; build one by hand with
204/// [`Receiver::new`].
205#[derive(Clone)]
206pub struct Receiver {
207 name: SignalName,
208 model: TypeId,
209 meta: &'static ModelMeta,
210 handler: Arc<Handler>,
211}
212
213impl Receiver {
214 /// A receiver for signal `name` of model `M`.
215 ///
216 /// `handler` gets the instance as `&M`; the downcast is checked, so a
217 /// receiver never sees another model.
218 ///
219 /// ```ignore
220 /// Receiver::new::<User, _>(SignalName::PostSave, |user, event| {
221 /// Box::pin(async move { Ok(()) })
222 /// })
223 /// ```
224 pub fn new<M, F>(name: SignalName, handler: F) -> Self
225 where
226 M: Model,
227 F: for<'a> Fn(&'a M, &'a SignalEvent<'a>) -> BoxFuture<'a, Result<(), SignalError>>
228 + Send
229 + Sync
230 + 'static,
231 {
232 let erased = erase(move |instance: &(dyn Any + Send + Sync), event| {
233 match instance.downcast_ref::<M>() {
234 Some(instance) => handler(instance, event),
235 None => Box::pin(async {
236 Err(SignalError::new("receiver invoked with a different model"))
237 }),
238 }
239 });
240 Self {
241 name,
242 model: TypeId::of::<M>(),
243 meta: M::META,
244 handler: Arc::new(erased),
245 }
246 }
247
248 /// The signal this receiver listens to.
249 pub fn name(&self) -> SignalName {
250 self.name
251 }
252
253 /// Metadata of the model this receiver is typed on.
254 pub fn model(&self) -> &'static ModelMeta {
255 self.meta
256 }
257
258 fn accepts(&self, name: SignalName, model: TypeId) -> bool {
259 self.name == name && self.model == model
260 }
261}
262
263impl std::fmt::Debug for Receiver {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 f.debug_struct("Receiver")
266 .field("signal", &self.name)
267 .field("model", &self.meta.name)
268 .finish_non_exhaustive()
269 }
270}
271
272/// Registry of [`Receiver`]s. Cheap to clone: clones share one list.
273#[derive(Clone, Default)]
274pub struct Signals {
275 receivers: Arc<RwLock<Vec<Receiver>>>,
276}
277
278impl Signals {
279 /// An empty registry.
280 pub fn new() -> Self {
281 Self::default()
282 }
283
284 /// Append `receiver`; it runs after those connected earlier.
285 ///
286 /// Takes `&self`: the registry is shared, so receivers connected after
287 /// [`Db::with_signals`] are seen by every clone.
288 pub fn connect(&self, receiver: Receiver) {
289 self.receivers
290 .write()
291 .unwrap_or_else(PoisonError::into_inner)
292 .push(receiver);
293 }
294
295 /// Number of connected receivers.
296 pub fn len(&self) -> usize {
297 self.read().len()
298 }
299
300 /// Whether no receiver is connected.
301 pub fn is_empty(&self) -> bool {
302 self.read().is_empty()
303 }
304
305 fn read(&self) -> RwLockReadGuard<'_, Vec<Receiver>> {
306 self.receivers
307 .read()
308 .unwrap_or_else(PoisonError::into_inner)
309 }
310
311 /// Whether any receiver listens to `name` for model `M`.
312 pub(crate) fn has_receivers<M: Model>(&self, name: SignalName) -> bool {
313 let model = TypeId::of::<M>();
314 self.read().iter().any(|r| r.accepts(name, model))
315 }
316
317 /// Run the matching receivers in connection order, stopping at the first
318 /// failure.
319 pub(crate) async fn send<M: Model>(
320 &self,
321 db: &Db,
322 instance: &M,
323 kind: SignalKind,
324 relation: Option<&'static str>,
325 pk_set: &[Value],
326 ) -> Result<(), OrmError> {
327 let (name, model) = (kind.name(), TypeId::of::<M>());
328 // Clone the matches so the lock is not held across `.await`.
329 let matching: Vec<Receiver> = self
330 .read()
331 .iter()
332 .filter(|r| r.accepts(name, model))
333 .cloned()
334 .collect();
335 if matching.is_empty() {
336 return Ok(());
337 }
338 let event = SignalEvent {
339 kind,
340 db,
341 model: M::META,
342 relation,
343 pk_set,
344 };
345 for receiver in matching {
346 (receiver.handler)(instance, &event).await?;
347 }
348 Ok(())
349 }
350}
351
352impl std::fmt::Debug for Signals {
353 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354 f.debug_struct("Signals")
355 .field("receivers", &self.len())
356 .finish()
357 }
358}