1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
use crate::proto;
use serde::{Deserialize, Serialize};
use serde_json::{map, Value};
use strum::{EnumVariantNames, VariantNames as _};

const KIND: &'static str = "kind";
const ENTITY_ID: &'static str = "entity_id";

/// A notification that can be stored, associated with a given recipient.
///
/// This struct is stored in the collab database as JSON, so it shouldn't be
/// changed in a backward-incompatible way. For example, when renaming a
/// variant, add a serde alias for the old name.
///
/// Most notification types have a special field which is aliased to
/// `entity_id`. This field is stored in its own database column, and can
/// be used to query the notification.
#[derive(Debug, Clone, PartialEq, Eq, EnumVariantNames, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum Notification {
    ContactRequest {
        #[serde(rename = "entity_id")]
        sender_id: u64,
    },
    ContactRequestAccepted {
        #[serde(rename = "entity_id")]
        responder_id: u64,
    },
    ChannelInvitation {
        #[serde(rename = "entity_id")]
        channel_id: u64,
        channel_name: String,
        inviter_id: u64,
    },
    ChannelMessageMention {
        #[serde(rename = "entity_id")]
        message_id: u64,
        sender_id: u64,
        channel_id: u64,
    },
}

impl Notification {
    pub fn to_proto(&self) -> proto::Notification {
        let mut value = serde_json::to_value(self).unwrap();
        let mut entity_id = None;
        let value = value.as_object_mut().unwrap();
        let Some(Value::String(kind)) = value.remove(KIND) else {
            unreachable!("kind is the enum tag")
        };
        if let map::Entry::Occupied(e) = value.entry(ENTITY_ID) {
            if e.get().is_u64() {
                entity_id = e.remove().as_u64();
            }
        }
        proto::Notification {
            kind,
            entity_id,
            content: serde_json::to_string(&value).unwrap(),
            ..Default::default()
        }
    }

    pub fn from_proto(notification: &proto::Notification) -> Option<Self> {
        let mut value = serde_json::from_str::<Value>(&notification.content).ok()?;
        let object = value.as_object_mut()?;
        object.insert(KIND.into(), notification.kind.to_string().into());
        if let Some(entity_id) = notification.entity_id {
            object.insert(ENTITY_ID.into(), entity_id.into());
        }
        serde_json::from_value(value).ok()
    }

    pub fn all_variant_names() -> &'static [&'static str] {
        Self::VARIANTS
    }
}

#[test]
fn test_notification() {
    // Notifications can be serialized and deserialized.
    for notification in [
        Notification::ContactRequest { sender_id: 1 },
        Notification::ContactRequestAccepted { responder_id: 2 },
        Notification::ChannelInvitation {
            channel_id: 100,
            channel_name: "the-channel".into(),
            inviter_id: 50,
        },
        Notification::ChannelMessageMention {
            sender_id: 200,
            channel_id: 30,
            message_id: 1,
        },
    ] {
        let message = notification.to_proto();
        let deserialized = Notification::from_proto(&message).unwrap();
        assert_eq!(deserialized, notification);
    }

    // When notifications are serialized, the `kind` and `actor_id` fields are
    // stored separately, and do not appear redundantly in the JSON.
    let notification = Notification::ContactRequest { sender_id: 1 };
    assert_eq!(notification.to_proto().content, "{}");
}