Skip to main content

ferro_notifications/channels/
slack.rs

1//! Slack notification channel.
2
3use serde::{Deserialize, Serialize};
4
5/// A Slack message for webhook notifications.
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7pub struct SlackMessage {
8    /// Main message text.
9    pub text: String,
10    /// Optional channel override.
11    pub channel: Option<String>,
12    /// Optional username override.
13    pub username: Option<String>,
14    /// Optional icon emoji (e.g., ":rocket:").
15    pub icon_emoji: Option<String>,
16    /// Optional icon URL.
17    pub icon_url: Option<String>,
18    /// Message attachments.
19    pub attachments: Vec<SlackAttachment>,
20}
21
22/// A Slack message attachment.
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24pub struct SlackAttachment {
25    /// Attachment fallback text.
26    pub fallback: Option<String>,
27    /// Attachment color (hex or named).
28    pub color: Option<String>,
29    /// Pretext shown above attachment.
30    pub pretext: Option<String>,
31    /// Author name.
32    pub author_name: Option<String>,
33    /// Author link.
34    pub author_link: Option<String>,
35    /// Attachment title.
36    pub title: Option<String>,
37    /// Title link.
38    pub title_link: Option<String>,
39    /// Main attachment text.
40    pub text: Option<String>,
41    /// Attachment fields.
42    pub fields: Vec<SlackField>,
43    /// Footer text.
44    pub footer: Option<String>,
45    /// Timestamp (Unix epoch).
46    pub ts: Option<i64>,
47}
48
49/// A field within a Slack attachment.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct SlackField {
52    /// Field title.
53    pub title: String,
54    /// Field value.
55    pub value: String,
56    /// Whether to display inline.
57    pub short: bool,
58}
59
60impl SlackMessage {
61    /// Create a new Slack message with text.
62    pub fn new(text: impl Into<String>) -> Self {
63        Self {
64            text: text.into(),
65            ..Default::default()
66        }
67    }
68
69    /// Set the channel.
70    pub fn channel(mut self, channel: impl Into<String>) -> Self {
71        self.channel = Some(channel.into());
72        self
73    }
74
75    /// Set the username.
76    pub fn username(mut self, username: impl Into<String>) -> Self {
77        self.username = Some(username.into());
78        self
79    }
80
81    /// Set the icon emoji.
82    pub fn icon_emoji(mut self, emoji: impl Into<String>) -> Self {
83        self.icon_emoji = Some(emoji.into());
84        self
85    }
86
87    /// Set the icon URL.
88    pub fn icon_url(mut self, url: impl Into<String>) -> Self {
89        self.icon_url = Some(url.into());
90        self
91    }
92
93    /// Add an attachment.
94    pub fn attachment(mut self, attachment: SlackAttachment) -> Self {
95        self.attachments.push(attachment);
96        self
97    }
98}
99
100impl SlackAttachment {
101    /// Create a new empty attachment.
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    /// Set the color.
107    pub fn color(mut self, color: impl Into<String>) -> Self {
108        self.color = Some(color.into());
109        self
110    }
111
112    /// Set the title.
113    pub fn title(mut self, title: impl Into<String>) -> Self {
114        self.title = Some(title.into());
115        self
116    }
117
118    /// Set the title link.
119    pub fn title_link(mut self, link: impl Into<String>) -> Self {
120        self.title_link = Some(link.into());
121        self
122    }
123
124    /// Set the text.
125    pub fn text(mut self, text: impl Into<String>) -> Self {
126        self.text = Some(text.into());
127        self
128    }
129
130    /// Add a field.
131    pub fn field(
132        mut self,
133        title: impl Into<String>,
134        value: impl Into<String>,
135        short: bool,
136    ) -> Self {
137        self.fields.push(SlackField {
138            title: title.into(),
139            value: value.into(),
140            short,
141        });
142        self
143    }
144
145    /// Set the footer.
146    pub fn footer(mut self, footer: impl Into<String>) -> Self {
147        self.footer = Some(footer.into());
148        self
149    }
150
151    /// Set the timestamp.
152    pub fn timestamp(mut self, ts: i64) -> Self {
153        self.ts = Some(ts);
154        self
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_slack_message_builder() {
164        let msg = SlackMessage::new("Hello, Slack!")
165            .channel("#general")
166            .username("MyBot")
167            .icon_emoji(":robot_face:");
168
169        assert_eq!(msg.text, "Hello, Slack!");
170        assert_eq!(msg.channel, Some("#general".into()));
171        assert_eq!(msg.username, Some("MyBot".into()));
172        assert_eq!(msg.icon_emoji, Some(":robot_face:".into()));
173    }
174
175    #[test]
176    fn test_slack_attachment_builder() {
177        let attachment = SlackAttachment::new()
178            .color("good")
179            .title("Order Placed")
180            .text("Order #123 has been placed")
181            .field("Customer", "John Doe", true)
182            .field("Amount", "$99.99", true);
183
184        assert_eq!(attachment.color, Some("good".into()));
185        assert_eq!(attachment.title, Some("Order Placed".into()));
186        assert_eq!(attachment.fields.len(), 2);
187        assert_eq!(attachment.fields[0].title, "Customer");
188        assert!(attachment.fields[0].short);
189    }
190
191    #[test]
192    fn test_slack_message_with_attachment() {
193        let msg = SlackMessage::new("New order received").attachment(
194            SlackAttachment::new()
195                .color("#36a64f")
196                .title("Order Details"),
197        );
198
199        assert_eq!(msg.attachments.len(), 1);
200    }
201}