1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
//! # rainier-notify
//!
//! Notifications: a message **to somebody**, over whichever channels
//! they can be reached on.
//!
//! ## This is not an event
//!
//! The two get confused because both fan out, and they fan out for different
//! reasons and to different things.
//!
//! | | An event | A notification |
//! |---|---|---|
//! | Says | "this happened" | "tell **them** this" |
//! | Recipient | none | exactly one, per send |
//! | Fan-out decided by | who subscribed, at boot | the notification and the recipient, per send |
//! | Payload | one shape | one per channel |
//! | Nobody handles it | normal | almost certainly a bug |
//! | Direction | producer → whoever is listening | producer → a named recipient |
//!
//! `PostPublished` is an event: a fact, with no opinion about who should hear
//! about it. `NewPostFromAuthorYouFollow` is a notification: it has a
//! recipient, and it knows it should be an email *and* a row in their bell
//! menu.
//!
//! They compose, and that composition is the usual wiring:
//!
//! ```ignore
//! // The event says what happened.
//! events.listen(|event: Arc<PostPublished>| async move {
//! let notifier = resolve::<Notifier>()?;
//!
//! // The notification says who to tell, and how.
//! for follower in followers_of(event.post.author_id).await? {
//! notifier.send(&follower, &NewPost { post: event.post.clone() }).await?;
//! }
//! Ok(())
//! });
//! ```
//!
//! Collapsing them loses something either way. An event with a recipient cannot
//! be listened to by anything else — an analytics listener has no business
//! receiving "tell Ada about this". A notification with no recipient cannot ask
//! Ada whether she wanted an email.
//!
//! ## The shape
//!
//! ```
//! use rainier_notify::{Channels, LogChannel, Notifiable, Notification, Notifier};
//!
//! struct User { id: u64, email: 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" => Some(self.email.clone()),
//! _ => None,
//! }
//! }
//! }
//!
//! struct InvoicePaid { amount: u64 }
//!
//! impl Notification<User> for InvoicePaid {
//! fn notification_name(&self) -> &'static str { "billing.invoice-paid" }
//!
//! fn via(&self, _: &User) -> Channels {
//! Channels::new().with::<LogChannel>()
//! }
//!
//! fn to_text(&self, user: &User) -> Option<String> {
//! Some(format!("Thanks — we received {} from user {}.", self.amount, user.id))
//! }
//! }
//!
//! # #[tokio::main] async fn main() -> rainier_support::Result<()> {
//! let notifier = Notifier::new().with(LogChannel);
//! let receipt = notifier.send(&User { id: 7, email: "ada@example.com".into() },
//! &InvoicePaid { amount: 4200 }).await?;
//!
//! assert!(receipt.delivered_anywhere());
//! # Ok(()) }
//! ```
//!
//! ## Channels are selected by type
//!
//! `Channels::new().with::<MailChannel>()`, not `["mail"]`. The same reasoning
//! as middleware and configuration keys: a misspelled
//! `"mial"` is a notification that silently goes nowhere, and deleting a
//! channel should break every notification that used it in the compiler.
//!
//! ## Failure is per channel
//!
//! An SMTP outage must not also cost the database row that puts the
//! notification in the recipient's bell menu, so a channel failing does not
//! stop the others. [`Receipt`] says what happened on each;
//! [`Receipt::into_result`] turns "nothing got through at all" into an error.
//!
//! A recipient with no address on a channel is **skipped**, not failed — a user
//! with no phone number should still get the email.
pub use ;
pub use ;
pub use Notifier;