origin_platform/
notifications.rs1use async_trait::async_trait;
2use origin_domain::Result;
3use serde::{Deserialize, Serialize};
4use std::fmt::Debug;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum Urgency {
9 Low,
10 #[default]
11 Normal,
12 Critical,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct NotificationAction {
21 pub id: String,
22 pub label: String,
23}
24
25impl NotificationAction {
26 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
27 Self {
28 id: id.into(),
29 label: label.into(),
30 }
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct Notification {
36 pub title: String,
37 pub body: Option<String>,
38 pub urgency: Urgency,
39 pub tag: Option<String>,
42 #[serde(default)]
49 pub actions: Vec<NotificationAction>,
50}
51
52impl Notification {
53 pub fn new(title: impl Into<String>) -> Self {
54 Self {
55 title: title.into(),
56 body: None,
57 urgency: Urgency::Normal,
58 tag: None,
59 actions: Vec::new(),
60 }
61 }
62
63 pub fn with_body(mut self, body: impl Into<String>) -> Self {
64 self.body = Some(body.into());
65 self
66 }
67
68 pub fn with_urgency(mut self, urgency: Urgency) -> Self {
69 self.urgency = urgency;
70 self
71 }
72
73 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
74 self.tag = Some(tag.into());
75 self
76 }
77
78 pub fn with_action(mut self, action: NotificationAction) -> Self {
80 self.actions.push(action);
81 self
82 }
83}
84
85#[async_trait]
90pub trait NotificationService: Debug + Send + Sync + 'static {
91 async fn notify(&self, notification: Notification) -> Result<()>;
92}
93
94#[derive(Debug, Clone, Copy, Default)]
96pub struct NoopNotificationService;
97
98#[async_trait]
99impl NotificationService for NoopNotificationService {
100 async fn notify(&self, notification: Notification) -> Result<()> {
101 tracing::debug!(title = %notification.title, "notification dropped (noop service)");
102 Ok(())
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn a_notification_carries_actions_with_stable_ids() {
112 let notification = Notification::new("Pull request ready")
113 .with_action(NotificationAction::new("open", "Open"))
114 .with_action(NotificationAction::new("mark-read", "Mark as read"));
115
116 assert_eq!(notification.actions.len(), 2);
117 assert_eq!(notification.actions[0].id, "open");
118 assert_eq!(notification.actions[1].id, "mark-read");
119 }
120
121 #[test]
122 fn a_plain_notification_has_no_actions() {
123 assert!(Notification::new("Ping").actions.is_empty());
124 }
125}