Skip to main content

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}