use std::any::TypeId;
use std::sync::Arc;
use rainier_mail::Mailable;
use rainier_support::Result;
use serde_json::Value;
use crate::channel::Channel;
pub trait Notifiable: Send + Sync {
fn notifiable_id(&self) -> String;
fn notifiable_type(&self) -> &'static str;
fn route_for(&self, channel: &str) -> Option<String>;
}
pub trait Notification<N: Notifiable + ?Sized>: Send + Sync {
fn notification_name(&self) -> &'static str;
fn via(&self, to: &N) -> Channels;
fn to_mail(&self, to: &N) -> Option<Box<dyn Mailable>> {
let _ = to;
None
}
fn to_text(&self, to: &N) -> Option<String> {
let _ = to;
None
}
fn to_data(&self, to: &N) -> Option<Value> {
let _ = to;
None
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Channels {
selected: Vec<(TypeId, &'static str)>,
}
impl Channels {
pub fn new() -> Self {
Self::default()
}
pub fn with<C: Channel>(mut self) -> Self {
self.push(TypeId::of::<C>(), short_type_name(std::any::type_name::<C>()));
self
}
fn push(&mut self, id: TypeId, name: &'static str) {
if !self.selected.iter().any(|(existing, _)| *existing == id) {
self.selected.push((id, name));
}
}
pub fn len(&self) -> usize {
self.selected.len()
}
pub fn is_empty(&self) -> bool {
self.selected.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (TypeId, &'static str)> + '_ {
self.selected.iter().copied()
}
pub fn names(&self) -> Vec<&'static str> {
self.selected.iter().map(|(_, name)| *name).collect()
}
}
fn short_type_name(full: &'static str) -> &'static str {
full.rsplit("::").next().unwrap_or(full)
}
#[derive(Clone)]
pub struct Delivery {
pub notification: &'static str,
pub recipient_id: String,
pub recipient_type: &'static str,
pub route: Option<String>,
mail: Option<Arc<dyn Mailable>>,
text: Option<String>,
data: Option<Value>,
}
impl Delivery {
pub fn render<N, T>(notification: &T, to: &N) -> Self
where
N: Notifiable + ?Sized,
T: Notification<N> + ?Sized,
{
Self {
notification: notification.notification_name(),
recipient_id: to.notifiable_id(),
recipient_type: to.notifiable_type(),
route: None,
mail: notification.to_mail(to).map(Arc::from),
text: notification.to_text(to),
data: notification.to_data(to),
}
}
pub fn routed_for(mut self, channel: &str, to: &(impl Notifiable + ?Sized)) -> Self {
self.route = to.route_for(channel);
self
}
pub fn with_route(&self, route: Option<String>) -> Self {
Self { route, ..self.clone() }
}
pub fn mail(&self) -> Option<&dyn Mailable> {
self.mail.as_deref()
}
pub fn text(&self) -> Option<&str> {
self.text.as_deref()
}
pub fn data(&self) -> Option<&Value> {
self.data.as_ref()
}
pub fn data_or_text(&self) -> Option<Value> {
self.data
.clone()
.or_else(|| self.text.as_ref().map(|text| serde_json::json!({ "message": text })))
}
pub fn nothing_to_send(&self, channel: &str, wanted: &str) -> rainier_support::Error {
rainier_support::Error::internal(format!(
"`{}` selected the `{channel}` channel but produced no {wanted}",
self.notification
))
}
pub fn no_route(&self, channel: &str) -> rainier_support::Error {
rainier_support::Error::internal(format!(
"`{} {}` has no `{channel}` address",
self.recipient_type, self.recipient_id
))
}
}
impl std::fmt::Debug for Delivery {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Delivery")
.field("notification", &self.notification)
.field("to", &format_args!("{} {}", self.recipient_type, self.recipient_id))
.field("route", &self.route)
.finish()
}
}
#[derive(Debug)]
pub enum Sent {
Delivered,
NoRoute,
NotRegistered,
Failed(rainier_support::Error),
}
impl Sent {
pub fn is_delivered(&self) -> bool {
matches!(self, Sent::Delivered)
}
pub fn is_failure(&self) -> bool {
matches!(self, Sent::Failed(_) | Sent::NotRegistered)
}
}
impl std::fmt::Display for Sent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Sent::Delivered => f.write_str("delivered"),
Sent::NoRoute => f.write_str("no address on that channel"),
Sent::NotRegistered => f.write_str("the channel is not registered"),
Sent::Failed(e) => write!(f, "failed: {}", e.message()),
}
}
}
#[derive(Debug, Default)]
pub struct Receipt {
pub outcomes: Vec<(&'static str, Sent)>,
}
impl Receipt {
pub fn delivered_anywhere(&self) -> bool {
self.outcomes.iter().any(|(_, sent)| sent.is_delivered())
}
pub fn delivered(&self) -> Vec<&'static str> {
self.outcomes
.iter()
.filter(|(_, sent)| sent.is_delivered())
.map(|(name, _)| *name)
.collect()
}
pub fn failures(&self) -> Vec<&'static str> {
self.outcomes.iter().filter(|(_, sent)| sent.is_failure()).map(|(name, _)| *name).collect()
}
pub fn into_result(self) -> Result<Self> {
if self.outcomes.is_empty() || self.delivered_anywhere() {
return Ok(self);
}
let reasons: Vec<String> =
self.outcomes.iter().map(|(name, sent)| format!("{name}: {sent}")).collect();
Err(rainier_support::Error::internal(format!(
"the notification reached nothing — {}",
reasons.join("; ")
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{LogChannel, MemoryChannel};
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" => self.email.clone(),
_ => Some(self.id.to_string()),
}
}
}
struct Welcome;
impl Notification<User> for Welcome {
fn notification_name(&self) -> &'static str {
"user.welcome"
}
fn via(&self, _: &User) -> Channels {
Channels::new().with::<LogChannel>()
}
fn to_text(&self, user: &User) -> Option<String> {
Some(format!("welcome, user {}", user.id))
}
}
#[test]
fn channels_keep_their_order() {
let channels = Channels::new().with::<LogChannel>().with::<MemoryChannel>();
assert_eq!(channels.names(), vec!["LogChannel", "MemoryChannel"]);
}
#[test]
fn selecting_a_channel_twice_only_sends_once() {
let channels = Channels::new().with::<LogChannel>().with::<LogChannel>();
assert_eq!(channels.len(), 1);
}
#[test]
fn a_notification_can_choose_no_channels() {
assert!(Channels::new().is_empty());
}
#[test]
fn rendering_happens_once_and_carries_the_recipients_identity() {
let user = User { id: 7, email: Some("a@b.c".into()) };
let delivery = Delivery::render(&Welcome, &user);
assert_eq!(delivery.notification, "user.welcome");
assert_eq!(delivery.recipient_id, "7");
assert_eq!(delivery.recipient_type, "User");
assert_eq!(delivery.text(), Some("welcome, user 7"));
assert!(delivery.mail().is_none(), "it did not implement `to_mail`");
}
#[test]
fn routing_asks_the_recipient_per_channel() {
let user = User { id: 7, email: Some("a@b.c".into()) };
let mail = Delivery::render(&Welcome, &user).routed_for("mail", &user);
assert_eq!(mail.route.as_deref(), Some("a@b.c"));
let none = User { id: 8, email: None };
let mail = Delivery::render(&Welcome, &none).routed_for("mail", &none);
assert_eq!(mail.route, None, "no address is a skip, not a failure");
}
#[test]
fn data_falls_back_to_wrapping_the_text() {
let user = User { id: 7, email: None };
let delivery = Delivery::render(&Welcome, &user);
assert_eq!(
delivery.data_or_text(),
Some(serde_json::json!({ "message": "welcome, user 7" }))
);
}
#[test]
fn a_receipt_is_ok_when_anything_got_through() {
let receipt = Receipt {
outcomes: vec![
("mail", Sent::Failed(rainier_support::Error::internal("smtp down"))),
("database", Sent::Delivered),
],
};
assert!(receipt.delivered_anywhere());
assert_eq!(receipt.failures(), vec!["mail"]);
assert!(receipt.into_result().is_ok(), "one channel down is not a failed send");
}
#[test]
fn a_receipt_fails_when_nothing_got_through() {
let receipt = Receipt {
outcomes: vec![("mail", Sent::Failed(rainier_support::Error::internal("smtp down")))],
};
let err = receipt.into_result().err().expect("should fail");
assert!(err.message().contains("smtp down"), "{}", err.message());
}
#[test]
fn sending_nowhere_at_all_is_not_a_failure() {
assert!(Receipt::default().into_result().is_ok());
}
}