rainier_notify/
notifier.rs1use 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#[derive(Default)]
46pub struct Notifier {
47 channels: HashMap<TypeId, Arc<dyn Channel>>,
48 order: Vec<TypeId>,
51}
52
53impl Notifier {
54 pub fn new() -> Self {
59 Self::default()
60 }
61
62 pub fn with<C: Channel>(self, channel: C) -> Self {
64 self.with_arc(Arc::new(channel))
65 }
66
67 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 pub fn has<C: Channel>(&self) -> bool {
81 self.channels.contains_key(&TypeId::of::<C>())
82 }
83
84 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 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 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 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 let name = channel.name();
134
135 let route = to.route_for(name);
136 if route.is_none() && wants_a_route(name) {
137 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 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
189fn 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 #[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 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 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 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 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}