Skip to main content

realtime/server/
types.rs

1use std::fmt::{Display, Formatter};
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use uuid::Uuid;
6
7use super::RealtimeError;
8
9pub type UserId = String;
10pub type Channel = String;
11pub type Event = String;
12pub type Payload = Value;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct ConnectionId(pub Uuid);
16
17impl ConnectionId {
18    pub fn new() -> Self {
19        Self(Uuid::new_v4())
20    }
21}
22
23impl Default for ConnectionId {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl Display for ConnectionId {
30    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
31        write!(f, "{}", self.0)
32    }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub struct ChannelName(pub String);
37
38impl ChannelName {
39    pub const MAX_LEN: usize = 128;
40
41    pub fn parse(raw: &str) -> Result<Self, RealtimeError> {
42        let trimmed = raw.trim();
43        if trimmed.is_empty() {
44            return Err(RealtimeError::bad_request("Channel name is required"));
45        }
46        if trimmed.len() > Self::MAX_LEN {
47            return Err(RealtimeError::bad_request("Channel name is too long"));
48        }
49        if !trimmed
50            .chars()
51            .all(|c| c.is_ascii_alphanumeric() || matches!(c, ':' | '_' | '-' | '.'))
52        {
53            return Err(RealtimeError::bad_request(
54                "Channel name contains invalid characters",
55            ));
56        }
57        Ok(Self(trimmed.to_string()))
58    }
59
60    pub fn as_str(&self) -> &str {
61        &self.0
62    }
63}
64
65impl Display for ChannelName {
66    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
67        f.write_str(self.as_str())
68    }
69}
70
71#[derive(Debug, Clone)]
72pub struct SessionAuth {
73    pub user_id: UserId,
74    pub roles: Vec<String>,
75}
76
77#[derive(Debug, Clone)]
78pub struct ConnectionMeta {
79    pub id: ConnectionId,
80    pub user_id: UserId,
81    pub roles: Vec<String>,
82    pub joined_at_unix: i64,
83}
84
85#[derive(Debug, Clone, Copy)]
86pub enum DisconnectReason {
87    ClientClosed,
88    SocketError,
89    HubUnavailable,
90    SlowConsumer,
91    IdleTimeout,
92    ProtocolError,
93}
94
95#[cfg(test)]
96mod tests {
97    use super::ChannelName;
98
99    #[test]
100    fn channel_name_parse_accepts_valid_symbols() {
101        let channel =
102            ChannelName::parse("todo:list:123_abc-xyz.test").expect("channel should parse");
103        assert_eq!(channel.as_str(), "todo:list:123_abc-xyz.test");
104    }
105
106    #[test]
107    fn channel_name_parse_rejects_empty_values() {
108        let err = ChannelName::parse("   ").expect_err("empty channel should fail");
109        assert_eq!(err.message(), "Channel name is required");
110    }
111
112    #[test]
113    fn channel_name_parse_rejects_invalid_characters() {
114        let err = ChannelName::parse("todo/list").expect_err("channel should fail");
115        assert_eq!(err.message(), "Channel name contains invalid characters");
116    }
117}