notification.rs

 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}
36
37impl Notification {
38    pub fn to_proto(&self) -> proto::Notification {
39        let mut value = serde_json::to_value(self).unwrap();
40        let mut entity_id = None;
41        let value = value.as_object_mut().unwrap();
42        let Some(Value::String(kind)) = value.remove(KIND) else {
43            unreachable!("kind is the enum tag")
44        };
45        if let map::Entry::Occupied(e) = value.entry(ENTITY_ID)
46            && e.get().is_u64()
47        {
48            entity_id = e.remove().as_u64();
49        }
50        proto::Notification {
51            kind,
52            entity_id,
53            content: serde_json::to_string(&value).unwrap(),
54            ..Default::default()
55        }
56    }
57
58    pub fn from_proto(notification: &proto::Notification) -> Option<Self> {
59        let mut value = serde_json::from_str::<Value>(&notification.content).ok()?;
60        let object = value.as_object_mut()?;
61        object.insert(KIND.into(), notification.kind.to_string().into());
62        if let Some(entity_id) = notification.entity_id {
63            object.insert(ENTITY_ID.into(), entity_id.into());
64        }
65        serde_json::from_value(value).ok()
66    }
67
68    pub fn all_variant_names() -> &'static [&'static str] {
69        Self::VARIANTS
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use crate::Notification;
76
77    #[test]
78    fn test_notification() {
79        // Notifications can be serialized and deserialized.
80        for notification in [
81            Notification::ContactRequest { sender_id: 1 },
82            Notification::ContactRequestAccepted { responder_id: 2 },
83            Notification::ChannelInvitation {
84                channel_id: 100,
85                channel_name: "the-channel".into(),
86                inviter_id: 50,
87            },
88        ] {
89            let message = notification.to_proto();
90            let deserialized = Notification::from_proto(&message).unwrap();
91            assert_eq!(deserialized, notification);
92        }
93
94        // When notifications are serialized, the `kind` and `actor_id` fields are
95        // stored separately, and do not appear redundantly in the JSON.
96        let notification = Notification::ContactRequest { sender_id: 1 };
97        assert_eq!(notification.to_proto().content, "{}");
98    }
99}