Skip to main content

ferro_notifications/
notifiable.rs

1//! Notifiable trait for entities that can receive notifications.
2
3use crate::channel::Channel;
4use crate::channels::DatabaseMessage;
5use crate::dispatcher::NotificationDispatcher;
6use crate::notification::Notification;
7use crate::Error;
8use async_trait::async_trait;
9
10/// Trait for entities that can receive notifications.
11///
12/// Implement this trait on your User model or any other entity
13/// that should be able to receive notifications.
14///
15/// # Example
16///
17/// ```rust,ignore
18/// use ferro_notifications::{Notifiable, Channel};
19///
20/// struct User {
21///     id: i64,
22///     email: String,
23///     slack_webhook: Option<String>,
24/// }
25///
26/// impl Notifiable for User {
27///     fn route_notification_for(&self, channel: Channel) -> Option<String> {
28///         match channel {
29///             Channel::Mail => Some(self.email.clone()),
30///             Channel::Slack => self.slack_webhook.clone(),
31///             Channel::Database => Some(self.id.to_string()),
32///             _ => None,
33///         }
34///     }
35/// }
36/// ```
37#[async_trait]
38pub trait Notifiable: Send + Sync {
39    /// Get the routing information for a specific channel.
40    ///
41    /// Returns the destination for the notification (email address,
42    /// webhook URL, user ID, etc.) or None if the channel is not
43    /// available for this entity.
44    fn route_notification_for(&self, channel: Channel) -> Option<String>;
45
46    /// Get the unique identifier for this notifiable entity.
47    /// Used for database notifications.
48    fn notifiable_id(&self) -> String {
49        "unknown".to_string()
50    }
51
52    /// Get the type name of this notifiable entity.
53    fn notifiable_type(&self) -> &'static str {
54        std::any::type_name::<Self>()
55    }
56
57    /// Send a notification to this entity.
58    ///
59    /// This is the main entry point for sending notifications.
60    /// It dispatches the notification through all configured channels.
61    async fn notify<N: Notification + 'static>(&self, notification: N) -> Result<(), Error> {
62        NotificationDispatcher::send(self, notification).await
63    }
64}
65
66/// Result of sending a notification through a channel.
67#[derive(Debug)]
68pub struct ChannelResult {
69    /// The channel that was used.
70    pub channel: Channel,
71    /// Whether the send was successful.
72    pub success: bool,
73    /// Error message if failed.
74    pub error: Option<String>,
75}
76
77impl ChannelResult {
78    /// Create a successful result.
79    pub fn success(channel: Channel) -> Self {
80        Self {
81            channel,
82            success: true,
83            error: None,
84        }
85    }
86
87    /// Create a failed result.
88    pub fn failure(channel: Channel, error: impl Into<String>) -> Self {
89        Self {
90            channel,
91            success: false,
92            error: Some(error.into()),
93        }
94    }
95}
96
97/// Extension trait for database notification storage.
98#[async_trait]
99pub trait DatabaseNotificationStore: Send + Sync {
100    /// Store a notification in the database.
101    async fn store(
102        &self,
103        notifiable_id: &str,
104        notifiable_type: &str,
105        notification_type: &str,
106        message: &DatabaseMessage,
107    ) -> Result<(), Error>;
108
109    /// Mark a notification as read.
110    async fn mark_as_read(&self, notification_id: &str) -> Result<(), Error>;
111
112    /// Get unread notifications for an entity.
113    async fn unread(&self, notifiable_id: &str) -> Result<Vec<StoredNotification>, Error>;
114}
115
116/// A notification stored in the database.
117#[derive(Debug, Clone)]
118pub struct StoredNotification {
119    /// Unique notification ID.
120    pub id: String,
121    /// Notifiable entity ID.
122    pub notifiable_id: String,
123    /// Notifiable entity type.
124    pub notifiable_type: String,
125    /// Notification type.
126    pub notification_type: String,
127    /// Notification data as JSON.
128    pub data: String,
129    /// When the notification was read (if at all).
130    pub read_at: Option<String>,
131    /// When the notification was created.
132    pub created_at: String,
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    struct TestUser {
140        id: i64,
141        email: String,
142    }
143
144    impl Notifiable for TestUser {
145        fn route_notification_for(&self, channel: Channel) -> Option<String> {
146            match channel {
147                Channel::Mail => Some(self.email.clone()),
148                Channel::Database => Some(self.id.to_string()),
149                _ => None,
150            }
151        }
152
153        fn notifiable_id(&self) -> String {
154            self.id.to_string()
155        }
156    }
157
158    #[test]
159    fn test_route_notification_for() {
160        let user = TestUser {
161            id: 42,
162            email: "test@example.com".to_string(),
163        };
164
165        assert_eq!(
166            user.route_notification_for(Channel::Mail),
167            Some("test@example.com".to_string())
168        );
169        assert_eq!(
170            user.route_notification_for(Channel::Database),
171            Some("42".to_string())
172        );
173        assert_eq!(user.route_notification_for(Channel::Slack), None);
174    }
175
176    #[test]
177    fn test_channel_result() {
178        let success = ChannelResult::success(Channel::Mail);
179        assert!(success.success);
180        assert!(success.error.is_none());
181
182        let failure = ChannelResult::failure(Channel::Slack, "Connection failed");
183        assert!(!failure.success);
184        assert_eq!(failure.error, Some("Connection failed".to_string()));
185    }
186}