Skip to main content

sockudo_http/
channel.rs

1use crate::{Result, SockudoError};
2use std::fmt;
3use std::str::FromStr;
4
5/// Type-safe channel representation
6#[derive(Debug, Clone, PartialEq, Eq, Hash)]
7pub enum Channel {
8    Public(PublicChannel),
9    Private(PrivateChannel),
10    Presence(PresenceChannel),
11    Encrypted(EncryptedChannel),
12}
13
14/// Public channel type
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct PublicChannel(ChannelName);
17
18/// Private channel type
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub struct PrivateChannel(ChannelName);
21
22/// Presence channel type
23#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub struct PresenceChannel(ChannelName);
25
26/// Encrypted channel type
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub struct EncryptedChannel(ChannelName);
29
30/// Validated channel name
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub struct ChannelName(String);
33
34impl ChannelName {
35    /// Creates a new channel name with validation
36    pub fn new(name: impl Into<String>) -> Result<Self> {
37        let name = name.into();
38        validate_channel_name(&name)?;
39        Ok(Self(name.to_owned()))
40    }
41
42    /// Gets the channel name as a string slice
43    pub fn as_str(&self) -> &str {
44        &self.0
45    }
46
47    /// Consumes self and returns the inner String
48    pub fn into_string(self) -> String {
49        self.0
50    }
51}
52
53impl AsRef<str> for ChannelName {
54    fn as_ref(&self) -> &str {
55        &self.0
56    }
57}
58
59impl fmt::Display for ChannelName {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(f, "{}", self.0)
62    }
63}
64
65/// Channel type enumeration
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ChannelType {
68    Public,
69    Private,
70    Presence,
71    Encrypted,
72}
73
74impl Channel {
75    /// Creates a channel from a string, automatically detecting the type
76    pub fn from_string(s: impl Into<String>) -> Result<Self> {
77        let s = s.into();
78
79        if s.starts_with("private-encrypted-") {
80            let name = s.strip_prefix("private-encrypted-").unwrap();
81            Ok(Channel::Encrypted(EncryptedChannel(ChannelName::new(
82                name,
83            )?)))
84        } else if s.starts_with("presence-") {
85            let name = s.strip_prefix("presence-").unwrap();
86            Ok(Channel::Presence(PresenceChannel(ChannelName::new(name)?)))
87        } else if s.starts_with("private-") {
88            let name = s.strip_prefix("private-").unwrap();
89            Ok(Channel::Private(PrivateChannel(ChannelName::new(name)?)))
90        } else {
91            Ok(Channel::Public(PublicChannel(ChannelName::new(s)?)))
92        }
93    }
94
95    /// Gets the full channel name including prefix
96    pub fn full_name(&self) -> String {
97        match self {
98            Channel::Public(ch) => ch.0.to_string(),
99            Channel::Private(ch) => format!("private-{}", ch.0),
100            Channel::Presence(ch) => format!("presence-{}", ch.0),
101            Channel::Encrypted(ch) => format!("private-encrypted-{}", ch.0),
102        }
103    }
104
105    /// Gets the channel type
106    pub fn channel_type(&self) -> ChannelType {
107        match self {
108            Channel::Public(_) => ChannelType::Public,
109            Channel::Private(_) => ChannelType::Private,
110            Channel::Presence(_) => ChannelType::Presence,
111            Channel::Encrypted(_) => ChannelType::Encrypted,
112        }
113    }
114
115    /// Checks if the channel requires authentication
116    pub fn requires_auth(&self) -> bool {
117        !matches!(self, Channel::Public(_))
118    }
119
120    /// Checks if the channel is encrypted
121    pub fn is_encrypted(&self) -> bool {
122        matches!(self, Channel::Encrypted(_))
123    }
124}
125
126impl fmt::Display for Channel {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        write!(f, "{}", self.full_name())
129    }
130}
131
132impl FromStr for Channel {
133    type Err = SockudoError;
134
135    fn from_str(s: &str) -> Result<Self> {
136        Channel::from_string(s)
137    }
138}
139
140// Implement convenience constructors for specific channel types
141impl PublicChannel {
142    pub fn new(name: impl Into<String>) -> Result<Self> {
143        Ok(Self(ChannelName::new(name)?))
144    }
145}
146
147impl PrivateChannel {
148    pub fn new(name: impl Into<String>) -> Result<Self> {
149        Ok(Self(ChannelName::new(name)?))
150    }
151}
152
153impl PresenceChannel {
154    pub fn new(name: impl Into<String>) -> Result<Self> {
155        Ok(Self(ChannelName::new(name)?))
156    }
157}
158
159impl EncryptedChannel {
160    pub fn new(name: impl Into<String>) -> Result<Self> {
161        Ok(Self(ChannelName::new(name)?))
162    }
163}
164
165// Validation moved here from util.rs
166use regex::Regex;
167use std::sync::LazyLock;
168
169static CHANNEL_NAME_PATTERN: LazyLock<Regex> =
170    LazyLock::new(|| Regex::new(r"^[A-Za-z0-9_\-=@,.;]+$").unwrap());
171
172fn validate_channel_name(name: &str) -> Result<()> {
173    if name.is_empty() {
174        return Err(SockudoError::Validation {
175            message: "Channel name cannot be empty".to_string(),
176        });
177    }
178
179    if name.len() > 200 {
180        return Err(SockudoError::Validation {
181            message: format!("Channel name too long: '{}' (max 200 characters)", name),
182        });
183    }
184
185    if !CHANNEL_NAME_PATTERN.is_match(name) {
186        return Err(SockudoError::Validation {
187            message: format!(
188                "Invalid channel name: '{}'. Must match pattern: [A-Za-z0-9_\\-=@,.;]+",
189                name
190            ),
191        });
192    }
193
194    Ok(())
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_channel_creation() {
203        assert!(Channel::from_string("test-channel").is_ok());
204        assert!(Channel::from_string("private-test").is_ok());
205        assert!(Channel::from_string("presence-test").is_ok());
206        assert!(Channel::from_string("private-encrypted-test").is_ok());
207    }
208
209    #[test]
210    fn test_channel_type_detection() {
211        let public = Channel::from_string("test").unwrap();
212        assert_eq!(public.channel_type(), ChannelType::Public);
213
214        let private = Channel::from_string("private-test").unwrap();
215        assert_eq!(private.channel_type(), ChannelType::Private);
216    }
217
218    #[test]
219    fn test_channel_name_validation() {
220        assert!(ChannelName::new("").is_err());
221        assert!(ChannelName::new("a".repeat(201)).is_err());
222        assert!(ChannelName::new("test channel").is_err()); // space not allowed
223        assert!(ChannelName::new("test-channel_123").is_ok());
224    }
225}