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    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>(&notification.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}