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}