Skip to main content

rainier_notify/
notifier.rs

1//! Sending — [`Notifier`].
2
3use std::any::TypeId;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use rainier_support::Result;
8
9use crate::channel::Channel;
10use crate::notification::{Delivery, Notifiable, Notification, Receipt, Sent};
11
12/// Holds the configured channels and fans a notification out across them.
13///
14/// Bound in the container; resolve it, or reach it through the `Notify`
15/// facade.
16///
17/// ```
18/// # use rainier_notify::{Channels, MemoryChannel, Notifiable, Notification, Notifier};
19/// # use std::sync::Arc;
20/// struct User(u64);
21///
22/// impl Notifiable for User {
23///     fn notifiable_id(&self) -> String { self.0.to_string() }
24///     fn notifiable_type(&self) -> &'static str { "User" }
25///     fn route_for(&self, _: &str) -> Option<String> { Some(self.0.to_string()) }
26/// }
27///
28/// struct Welcome;
29///
30/// impl Notification<User> for Welcome {
31///     fn notification_name(&self) -> &'static str { "user.welcome" }
32///     fn via(&self, _: &User) -> Channels { Channels::new().with::<MemoryChannel>() }
33///     fn to_text(&self, _: &User) -> Option<String> { Some("hello".into()) }
34/// }
35///
36/// # #[tokio::main] async fn main() -> rainier_support::Result<()> {
37/// let captured = Arc::new(MemoryChannel::new());
38/// let notifier = Notifier::new().with_arc(Arc::clone(&captured));
39///
40/// notifier.send(&User(7), &Welcome).await?;
41///
42/// assert!(captured.sent_to("user.welcome", "7"));
43/// # Ok(()) }
44/// ```
45#[derive(Default)]
46pub struct Notifier {
47    channels: HashMap<TypeId, Arc<dyn Channel>>,
48    /// Insertion order, so a receipt reads the way the channels were
49    /// registered rather than however the map happened to hash them.
50    order: Vec<TypeId>,
51}
52
53impl Notifier {
54    /// A notifier with no channels.
55    ///
56    /// Sending through it delivers nothing and says so per channel — see
57    /// [`Sent::NotRegistered`].
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Register a channel.
63    pub fn with<C: Channel>(self, channel: C) -> Self {
64        self.with_arc(Arc::new(channel))
65    }
66
67    /// Register an already-shared channel.
68    ///
69    /// For one a test holds a handle to — a [`MemoryChannel`](crate::MemoryChannel)
70    /// it will assert on.
71    pub fn with_arc<C: Channel>(mut self, channel: Arc<C>) -> Self {
72        let id = TypeId::of::<C>();
73        if self.channels.insert(id, channel).is_none() {
74            self.order.push(id);
75        }
76        self
77    }
78
79    /// Whether a channel of this type is registered.
80    pub fn has<C: Channel>(&self) -> bool {
81        self.channels.contains_key(&TypeId::of::<C>())
82    }
83
84    /// The registered channels' names, in registration order.
85    pub fn channel_names(&self) -> Vec<&'static str> {
86        self.order.iter().filter_map(|id| self.channels.get(id)).map(|c| c.name()).collect()
87    }
88
89    /// Send `notification` to `to`.
90    ///
91    /// Every channel the notification selected is tried. **A channel failing
92    /// does not stop the others** — an SMTP outage should not also cost the
93    /// database row that puts the notification in the recipient's bell menu.
94    ///
95    /// The [`Receipt`] says what happened on each. `Ok` unless *every* channel
96    /// failed; call [`Receipt::into_result`] to make "nothing got through" an
97    /// error at the call site.
98    pub async fn send<N, T>(&self, to: &N, notification: &T) -> Result<Receipt>
99    where
100        N: Notifiable + ?Sized,
101        T: Notification<N> + ?Sized,
102    {
103        let channels = notification.via(to);
104        if channels.is_empty() {
105            tracing::debug!(
106                notification = notification.notification_name(),
107                "no channels selected; nothing sent"
108            );
109            return Ok(Receipt::default());
110        }
111
112        // Rendered once, shared across every channel. A notification that
113        // implements all three forms should not pay for them per channel.
114        let rendered = Delivery::render(notification, to);
115        let mut receipt = Receipt::default();
116
117        for (id, type_name) in channels.iter() {
118            let Some(channel) = self.channels.get(&id) else {
119                // Louder than a skip: the notification asked for a channel the
120                // application never wired, and quietly dropping it is how a
121                // password reset goes nowhere.
122                tracing::error!(
123                    notification = rendered.notification,
124                    channel = type_name,
125                    "the notification selected a channel that is not registered"
126                );
127                receipt.outcomes.push((type_name, Sent::NotRegistered));
128                continue;
129            };
130
131            // The channel's own name, from the configured instance — that is
132            // what `route_for` is asked about, and what a receipt should say.
133            let name = channel.name();
134
135            let route = to.route_for(name);
136            if route.is_none() && wants_a_route(name) {
137                // A user with no phone number should still get the email.
138                tracing::debug!(
139                    notification = rendered.notification,
140                    channel = name,
141                    "the recipient has no address on this channel; skipping"
142                );
143                receipt.outcomes.push((name, Sent::NoRoute));
144                continue;
145            }
146
147            let delivery = rendered.with_route(route);
148
149            match channel.send(&delivery).await {
150                Ok(()) => receipt.outcomes.push((name, Sent::Delivered)),
151                Err(e) => {
152                    tracing::error!(
153                        notification = rendered.notification,
154                        channel = name,
155                        error = %e.message(),
156                        "the notification failed on this channel"
157                    );
158                    receipt.outcomes.push((name, Sent::Failed(e)));
159                }
160            }
161        }
162
163        Ok(receipt)
164    }
165
166    /// Send the same notification to several recipients.
167    ///
168    /// One receipt each, in order. A failure for one recipient does not stop
169    /// the rest — the same reasoning as a failing channel, one level up.
170    pub async fn send_to_many<N, T>(&self, to: &[&N], notification: &T) -> Vec<Result<Receipt>>
171    where
172        N: Notifiable + ?Sized,
173        T: Notification<N> + ?Sized,
174    {
175        let mut receipts = Vec::with_capacity(to.len());
176        for recipient in to {
177            receipts.push(self.send(*recipient, notification).await);
178        }
179        receipts
180    }
181}
182
183impl std::fmt::Debug for Notifier {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        f.debug_struct("Notifier").field("channels", &self.channel_names()).finish()
186    }
187}
188
189/// Whether a channel is useless without an address.
190///
191/// The `log`, `memory` and `database` channels address the recipient by their
192/// **id**, which every notifiable has. Mail, SMS and a webhook need something
193/// the recipient may simply not have, and missing it is a skip rather than a
194/// failure.
195fn wants_a_route(channel: &str) -> bool {
196    !matches!(channel, "log" | "memory" | "database" | "broadcast")
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::channel::{LogChannel, MemoryChannel};
203    use crate::notification::Channels;
204    use rainier_support::{BoxFuture, Error};
205
206    struct User {
207        id: u64,
208        email: Option<String>,
209    }
210
211    impl Notifiable for User {
212        fn notifiable_id(&self) -> String {
213            self.id.to_string()
214        }
215        fn notifiable_type(&self) -> &'static str {
216            "User"
217        }
218        fn route_for(&self, channel: &str) -> Option<String> {
219            match channel {
220                "mail" | "broken" => self.email.clone(),
221                _ => Some(self.id.to_string()),
222            }
223        }
224    }
225
226    /// A channel that always fails, to prove one does not stop the others.
227    #[derive(Default)]
228    struct BrokenChannel;
229
230    impl Channel for BrokenChannel {
231        fn name(&self) -> &'static str {
232            "broken"
233        }
234        fn send<'a>(&'a self, _: &'a Delivery) -> BoxFuture<'a, Result<()>> {
235            Box::pin(async { Err(Error::internal("the pigeon did not come back")) })
236        }
237    }
238
239    struct Welcome(Channels);
240
241    impl Notification<User> for Welcome {
242        fn notification_name(&self) -> &'static str {
243            "user.welcome"
244        }
245        fn via(&self, _: &User) -> Channels {
246            self.0.clone()
247        }
248        fn to_text(&self, _: &User) -> Option<String> {
249            Some("hello".into())
250        }
251    }
252
253    fn user() -> User {
254        User { id: 7, email: Some("ada@example.com".into()) }
255    }
256
257    #[tokio::test]
258    async fn it_sends_on_every_selected_channel() {
259        let captured = Arc::new(MemoryChannel::new());
260        let notifier = Notifier::new().with(LogChannel).with_arc(Arc::clone(&captured));
261
262        let receipt = notifier
263            .send(&user(), &Welcome(Channels::new().with::<LogChannel>().with::<MemoryChannel>()))
264            .await
265            .unwrap();
266
267        assert_eq!(receipt.outcomes.len(), 2);
268        assert!(receipt.delivered_anywhere());
269        assert!(captured.sent_to("user.welcome", "7"));
270    }
271
272    #[tokio::test]
273    async fn one_channel_failing_does_not_stop_the_others() {
274        // An SMTP outage must not also cost the database row that puts the
275        // notification in the recipient's bell menu.
276        let captured = Arc::new(MemoryChannel::new());
277        let notifier = Notifier::new().with(BrokenChannel).with_arc(Arc::clone(&captured));
278
279        let receipt = notifier
280            .send(
281                &user(),
282                &Welcome(Channels::new().with::<BrokenChannel>().with::<MemoryChannel>()),
283            )
284            .await
285            .unwrap();
286
287        assert_eq!(receipt.failures(), vec!["broken"]);
288        assert!(captured.sent_to("user.welcome", "7"), "the other channel still ran");
289        assert!(receipt.into_result().is_ok(), "one failure is not a failed send");
290    }
291
292    #[tokio::test]
293    async fn every_channel_failing_is_a_failed_send() {
294        let notifier = Notifier::new().with(BrokenChannel);
295
296        let receipt = notifier
297            .send(&user(), &Welcome(Channels::new().with::<BrokenChannel>()))
298            .await
299            .unwrap();
300
301        assert!(!receipt.delivered_anywhere());
302        assert!(receipt.into_result().is_err());
303    }
304
305    #[tokio::test]
306    async fn a_recipient_with_no_address_is_skipped_not_failed() {
307        let notifier = Notifier::new().with(BrokenChannel);
308        let unreachable = User { id: 8, email: None };
309
310        let receipt = notifier
311            .send(&unreachable, &Welcome(Channels::new().with::<BrokenChannel>()))
312            .await
313            .unwrap();
314
315        assert!(matches!(receipt.outcomes[0], ("broken", Sent::NoRoute)));
316        assert!(receipt.failures().is_empty(), "a skip is not a failure");
317    }
318
319    #[tokio::test]
320    async fn a_channel_addressed_by_id_needs_no_route() {
321        // `log`, `memory` and `database` address the recipient by their id,
322        // which every notifiable has.
323        let captured = Arc::new(MemoryChannel::new());
324        let notifier = Notifier::new().with_arc(Arc::clone(&captured));
325        let unreachable = User { id: 8, email: None };
326
327        notifier
328            .send(&unreachable, &Welcome(Channels::new().with::<MemoryChannel>()))
329            .await
330            .unwrap();
331
332        assert!(captured.sent_to("user.welcome", "8"));
333    }
334
335    #[tokio::test]
336    async fn selecting_an_unregistered_channel_is_reported_not_ignored() {
337        // Quietly dropping it is how a password reset goes nowhere.
338        let notifier = Notifier::new().with(LogChannel);
339
340        let receipt = notifier
341            .send(&user(), &Welcome(Channels::new().with::<MemoryChannel>()))
342            .await
343            .unwrap();
344
345        // The *type* name, because an unregistered channel has no instance to
346        // ask for its own.
347        assert!(matches!(receipt.outcomes[0], ("MemoryChannel", Sent::NotRegistered)));
348        assert!(receipt.into_result().is_err());
349    }
350
351    #[tokio::test]
352    async fn a_notification_that_selects_nothing_sends_nothing() {
353        let notifier = Notifier::new().with(LogChannel);
354
355        let receipt = notifier.send(&user(), &Welcome(Channels::new())).await.unwrap();
356
357        assert!(receipt.outcomes.is_empty());
358        assert!(receipt.into_result().is_ok(), "opting out of everything is not an error");
359    }
360
361    #[tokio::test]
362    async fn several_recipients_get_one_receipt_each() {
363        let captured = Arc::new(MemoryChannel::new());
364        let notifier = Notifier::new().with_arc(Arc::clone(&captured));
365
366        let (a, b) = (User { id: 1, email: None }, User { id: 2, email: None });
367        let receipts = notifier
368            .send_to_many(&[&a, &b], &Welcome(Channels::new().with::<MemoryChannel>()))
369            .await;
370
371        assert_eq!(receipts.len(), 2);
372        assert!(captured.sent_to("user.welcome", "1"));
373        assert!(captured.sent_to("user.welcome", "2"));
374    }
375
376    #[test]
377    fn the_registered_channels_keep_their_order() {
378        let notifier = Notifier::new().with(LogChannel).with(MemoryChannel::new());
379        assert_eq!(notifier.channel_names(), vec!["log", "memory"]);
380        assert!(notifier.has::<LogChannel>());
381        assert!(!notifier.has::<BrokenChannel>());
382    }
383}