Skip to main content

ferro_notifications/
channel.rs

1//! Notification channel abstraction.
2
3use serde::{Deserialize, Serialize};
4
5/// Available notification channels.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum Channel {
9    /// Email notifications via SMTP or Resend.
10    Mail,
11    /// Database-persisted in-app notifications.
12    Database,
13    /// Slack webhook notifications.
14    Slack,
15    /// WhatsApp Cloud API notifications (per CONTEXT.md D-01).
16    ///
17    /// Serializes as `"whatsapp"` (explicit rename for clarity; matches `lowercase` output).
18    #[serde(rename = "whatsapp")]
19    WhatsApp,
20    /// Real-time in-app SSE notifications (per CONTEXT.md D-01).
21    ///
22    /// Serializes as `"in_app"` — overrides the enum-level `lowercase` rule
23    /// which would otherwise produce `"inapp"`.
24    #[serde(rename = "in_app")]
25    InApp,
26    /// SMS notifications (placeholder — no adapter ships in this phase per ARCH-FINDING-03).
27    Sms,
28    /// Push notifications (placeholder — no adapter ships in this phase).
29    Push,
30}
31
32impl Channel {
33    /// Get channel name as string.
34    pub fn as_str(&self) -> &'static str {
35        match self {
36            Channel::Mail => "mail",
37            Channel::Database => "database",
38            Channel::Slack => "slack",
39            Channel::WhatsApp => "whatsapp",
40            Channel::InApp => "in_app",
41            Channel::Sms => "sms",
42            Channel::Push => "push",
43        }
44    }
45}
46
47impl std::fmt::Display for Channel {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        write!(f, "{}", self.as_str())
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn test_channel_as_str() {
59        assert_eq!(Channel::Mail.as_str(), "mail");
60        assert_eq!(Channel::Database.as_str(), "database");
61        assert_eq!(Channel::Slack.as_str(), "slack");
62        assert_eq!(Channel::WhatsApp.as_str(), "whatsapp");
63        assert_eq!(Channel::InApp.as_str(), "in_app");
64        assert_eq!(Channel::Sms.as_str(), "sms");
65        assert_eq!(Channel::Push.as_str(), "push");
66    }
67
68    #[test]
69    fn test_channel_display() {
70        assert_eq!(format!("{}", Channel::Mail), "mail");
71        assert_eq!(format!("{}", Channel::WhatsApp), "whatsapp");
72        assert_eq!(format!("{}", Channel::InApp), "in_app");
73    }
74
75    #[test]
76    fn test_channel_serialization() {
77        // Forward
78        assert_eq!(
79            serde_json::to_string(&Channel::WhatsApp).unwrap(),
80            "\"whatsapp\""
81        );
82        assert_eq!(
83            serde_json::to_string(&Channel::InApp).unwrap(),
84            "\"in_app\""
85        );
86        assert_eq!(serde_json::to_string(&Channel::Mail).unwrap(), "\"mail\"");
87        assert_eq!(serde_json::to_string(&Channel::Sms).unwrap(), "\"sms\"");
88        assert_eq!(serde_json::to_string(&Channel::Push).unwrap(), "\"push\"");
89    }
90
91    #[test]
92    fn test_channel_deserialization() {
93        // Round-trip
94        assert_eq!(
95            serde_json::from_str::<Channel>("\"whatsapp\"").unwrap(),
96            Channel::WhatsApp
97        );
98        assert_eq!(
99            serde_json::from_str::<Channel>("\"in_app\"").unwrap(),
100            Channel::InApp
101        );
102        assert_eq!(
103            serde_json::from_str::<Channel>("\"mail\"").unwrap(),
104            Channel::Mail
105        );
106        assert_eq!(
107            serde_json::from_str::<Channel>("\"database\"").unwrap(),
108            Channel::Database
109        );
110        assert_eq!(
111            serde_json::from_str::<Channel>("\"slack\"").unwrap(),
112            Channel::Slack
113        );
114        // The literal "inapp" must NOT deserialize to InApp (regression guard for ARCH-FINDING-05/serde-trap)
115        assert!(
116            serde_json::from_str::<Channel>("\"inapp\"").is_err(),
117            "InApp must not accept the unrenamed `inapp` form"
118        );
119    }
120}