1use crate::proto;
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, map};
4use strum::VariantNames;
5
6const KIND: &str = "kind";
7const ENTITY_ID: &str = "entity_id";
8
9/// A notification that can be stored, associated with a given recipient.
10///
11/// This struct is stored in the collab database as JSON, so it shouldn't be
12/// changed in a backward-incompatible way. For example, when renaming a
13/// variant, add a serde alias for the old name.
14///
15/// Most notification types have a special field which is aliased to
16/// `entity_id`. This field is stored in its own database column, and can
17/// be used to query the notification.
18#[derive(Debug, Clone, PartialEq, Eq, VariantNames, Serialize, Deserialize)]
19#[serde(tag = "kind")]
20pub enum Notification {
21 ContactRequest {
22 #[serde(rename = "entity_id")]
23 sender_id: u64,
24 },
25 ContactRequestAccepted {
26 #[serde(rename = "entity_id")]
27 responder_id: u64,
28 },
29 ChannelInvitation {
30 #[serde(rename = "entity_id")]
31 channel_id: u64,
32 channel_name: String,
33 inviter_id: u64,
34 },
35 ChannelMessageMention {
36 #[serde(rename = "entity_id")]
37 message_id: u64,
38 sender_id: u64,
39 channel_id: u64,
40 },
41}
42
43impl Notification {
44 pub fn to_proto(&self) -> proto::Notification {
45 let mut value = serde_json::to_value(self).unwrap();
46 let mut entity_id = None;
47 let value = value.as_object_mut().unwrap();
48 let Some(Value::String(kind)) = value.remove(KIND) else {
49 unreachable!("kind is the enum tag")
50 };
51 if let map::Entry::Occupied(e) = value.entry(ENTITY_ID)
52 && e.get().is_u64()
53 {
54 entity_id = e.remove().as_u64();
55 }
56 proto::Notification {
57 kind,
58 entity_id,
59 content: serde_json::to_string(&value).unwrap(),
60 ..Default::default()
61 }
62 }
63
64 pub fn from_proto(notification: &proto::Notification) -> Option<Self> {
65 let mut value = serde_json::from_str::<Value>(¬ification.content).ok()?;
66 let object = value.as_object_mut()?;
67 object.insert(KIND.into(), notification.kind.to_string().into());
68 if let Some(entity_id) = notification.entity_id {
69 object.insert(ENTITY_ID.into(), entity_id.into());
70 }
71 serde_json::from_value(value).ok()
72 }
73
74 pub fn all_variant_names() -> &'static [&'static str] {
75 Self::VARIANTS
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use crate::Notification;
82
83 #[test]
84 fn test_notification() {
85 // Notifications can be serialized and deserialized.
86 for notification in [
87 Notification::ContactRequest { sender_id: 1 },
88 Notification::ContactRequestAccepted { responder_id: 2 },
89 Notification::ChannelInvitation {
90 channel_id: 100,
91 channel_name: "the-channel".into(),
92 inviter_id: 50,
93 },
94 Notification::ChannelMessageMention {
95 sender_id: 200,
96 channel_id: 30,
97 message_id: 1,
98 },
99 ] {
100 let message = notification.to_proto();
101 let deserialized = Notification::from_proto(&message).unwrap();
102 assert_eq!(deserialized, notification);
103 }
104
105 // When notifications are serialized, the `kind` and `actor_id` fields are
106 // stored separately, and do not appear redundantly in the JSON.
107 let notification = Notification::ContactRequest { sender_id: 1 };
108 assert_eq!(notification.to_proto().content, "{}");
109 }
110}