use std::any::TypeId;
use std::collections::HashMap;
use std::sync::Arc;
use rainier_support::Result;
use crate::channel::Channel;
use crate::notification::{Delivery, Notifiable, Notification, Receipt, Sent};
#[derive(Default)]
pub struct Notifier {
channels: HashMap<TypeId, Arc<dyn Channel>>,
order: Vec<TypeId>,
}
impl Notifier {
pub fn new() -> Self {
Self::default()
}
pub fn with<C: Channel>(self, channel: C) -> Self {
self.with_arc(Arc::new(channel))
}
pub fn with_arc<C: Channel>(mut self, channel: Arc<C>) -> Self {
let id = TypeId::of::<C>();
if self.channels.insert(id, channel).is_none() {
self.order.push(id);
}
self
}
pub fn has<C: Channel>(&self) -> bool {
self.channels.contains_key(&TypeId::of::<C>())
}
pub fn channel_names(&self) -> Vec<&'static str> {
self.order.iter().filter_map(|id| self.channels.get(id)).map(|c| c.name()).collect()
}
pub async fn send<N, T>(&self, to: &N, notification: &T) -> Result<Receipt>
where
N: Notifiable + ?Sized,
T: Notification<N> + ?Sized,
{
let channels = notification.via(to);
if channels.is_empty() {
tracing::debug!(
notification = notification.notification_name(),
"no channels selected; nothing sent"
);
return Ok(Receipt::default());
}
let rendered = Delivery::render(notification, to);
let mut receipt = Receipt::default();
for (id, type_name) in channels.iter() {
let Some(channel) = self.channels.get(&id) else {
tracing::error!(
notification = rendered.notification,
channel = type_name,
"the notification selected a channel that is not registered"
);
receipt.outcomes.push((type_name, Sent::NotRegistered));
continue;
};
let name = channel.name();
let route = to.route_for(name);
if route.is_none() && wants_a_route(name) {
tracing::debug!(
notification = rendered.notification,
channel = name,
"the recipient has no address on this channel; skipping"
);
receipt.outcomes.push((name, Sent::NoRoute));
continue;
}
let delivery = rendered.with_route(route);
match channel.send(&delivery).await {
Ok(()) => receipt.outcomes.push((name, Sent::Delivered)),
Err(e) => {
tracing::error!(
notification = rendered.notification,
channel = name,
error = %e.message(),
"the notification failed on this channel"
);
receipt.outcomes.push((name, Sent::Failed(e)));
}
}
}
Ok(receipt)
}
pub async fn send_to_many<N, T>(&self, to: &[&N], notification: &T) -> Vec<Result<Receipt>>
where
N: Notifiable + ?Sized,
T: Notification<N> + ?Sized,
{
let mut receipts = Vec::with_capacity(to.len());
for recipient in to {
receipts.push(self.send(*recipient, notification).await);
}
receipts
}
}
impl std::fmt::Debug for Notifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Notifier").field("channels", &self.channel_names()).finish()
}
}
fn wants_a_route(channel: &str) -> bool {
!matches!(channel, "log" | "memory" | "database" | "broadcast")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::channel::{LogChannel, MemoryChannel};
use crate::notification::Channels;
use rainier_support::{BoxFuture, Error};
struct User {
id: u64,
email: Option<String>,
}
impl Notifiable for User {
fn notifiable_id(&self) -> String {
self.id.to_string()
}
fn notifiable_type(&self) -> &'static str {
"User"
}
fn route_for(&self, channel: &str) -> Option<String> {
match channel {
"mail" | "broken" => self.email.clone(),
_ => Some(self.id.to_string()),
}
}
}
#[derive(Default)]
struct BrokenChannel;
impl Channel for BrokenChannel {
fn name(&self) -> &'static str {
"broken"
}
fn send<'a>(&'a self, _: &'a Delivery) -> BoxFuture<'a, Result<()>> {
Box::pin(async { Err(Error::internal("the pigeon did not come back")) })
}
}
struct Welcome(Channels);
impl Notification<User> for Welcome {
fn notification_name(&self) -> &'static str {
"user.welcome"
}
fn via(&self, _: &User) -> Channels {
self.0.clone()
}
fn to_text(&self, _: &User) -> Option<String> {
Some("hello".into())
}
}
fn user() -> User {
User { id: 7, email: Some("ada@example.com".into()) }
}
#[tokio::test]
async fn it_sends_on_every_selected_channel() {
let captured = Arc::new(MemoryChannel::new());
let notifier = Notifier::new().with(LogChannel).with_arc(Arc::clone(&captured));
let receipt = notifier
.send(&user(), &Welcome(Channels::new().with::<LogChannel>().with::<MemoryChannel>()))
.await
.unwrap();
assert_eq!(receipt.outcomes.len(), 2);
assert!(receipt.delivered_anywhere());
assert!(captured.sent_to("user.welcome", "7"));
}
#[tokio::test]
async fn one_channel_failing_does_not_stop_the_others() {
let captured = Arc::new(MemoryChannel::new());
let notifier = Notifier::new().with(BrokenChannel).with_arc(Arc::clone(&captured));
let receipt = notifier
.send(
&user(),
&Welcome(Channels::new().with::<BrokenChannel>().with::<MemoryChannel>()),
)
.await
.unwrap();
assert_eq!(receipt.failures(), vec!["broken"]);
assert!(captured.sent_to("user.welcome", "7"), "the other channel still ran");
assert!(receipt.into_result().is_ok(), "one failure is not a failed send");
}
#[tokio::test]
async fn every_channel_failing_is_a_failed_send() {
let notifier = Notifier::new().with(BrokenChannel);
let receipt = notifier
.send(&user(), &Welcome(Channels::new().with::<BrokenChannel>()))
.await
.unwrap();
assert!(!receipt.delivered_anywhere());
assert!(receipt.into_result().is_err());
}
#[tokio::test]
async fn a_recipient_with_no_address_is_skipped_not_failed() {
let notifier = Notifier::new().with(BrokenChannel);
let unreachable = User { id: 8, email: None };
let receipt = notifier
.send(&unreachable, &Welcome(Channels::new().with::<BrokenChannel>()))
.await
.unwrap();
assert!(matches!(receipt.outcomes[0], ("broken", Sent::NoRoute)));
assert!(receipt.failures().is_empty(), "a skip is not a failure");
}
#[tokio::test]
async fn a_channel_addressed_by_id_needs_no_route() {
let captured = Arc::new(MemoryChannel::new());
let notifier = Notifier::new().with_arc(Arc::clone(&captured));
let unreachable = User { id: 8, email: None };
notifier
.send(&unreachable, &Welcome(Channels::new().with::<MemoryChannel>()))
.await
.unwrap();
assert!(captured.sent_to("user.welcome", "8"));
}
#[tokio::test]
async fn selecting_an_unregistered_channel_is_reported_not_ignored() {
let notifier = Notifier::new().with(LogChannel);
let receipt = notifier
.send(&user(), &Welcome(Channels::new().with::<MemoryChannel>()))
.await
.unwrap();
assert!(matches!(receipt.outcomes[0], ("MemoryChannel", Sent::NotRegistered)));
assert!(receipt.into_result().is_err());
}
#[tokio::test]
async fn a_notification_that_selects_nothing_sends_nothing() {
let notifier = Notifier::new().with(LogChannel);
let receipt = notifier.send(&user(), &Welcome(Channels::new())).await.unwrap();
assert!(receipt.outcomes.is_empty());
assert!(receipt.into_result().is_ok(), "opting out of everything is not an error");
}
#[tokio::test]
async fn several_recipients_get_one_receipt_each() {
let captured = Arc::new(MemoryChannel::new());
let notifier = Notifier::new().with_arc(Arc::clone(&captured));
let (a, b) = (User { id: 1, email: None }, User { id: 2, email: None });
let receipts = notifier
.send_to_many(&[&a, &b], &Welcome(Channels::new().with::<MemoryChannel>()))
.await;
assert_eq!(receipts.len(), 2);
assert!(captured.sent_to("user.welcome", "1"));
assert!(captured.sent_to("user.welcome", "2"));
}
#[test]
fn the_registered_channels_keep_their_order() {
let notifier = Notifier::new().with(LogChannel).with(MemoryChannel::new());
assert_eq!(notifier.channel_names(), vec!["log", "memory"]);
assert!(notifier.has::<LogChannel>());
assert!(!notifier.has::<BrokenChannel>());
}
}