Skip to main content

nntp_proxy/types/
protocol.rs

1//! Protocol-related type-safe wrappers for NNTP primitives
2
3use serde::{Deserialize, Serialize};
4use std::borrow::{Borrow, Cow};
5use std::fmt;
6use std::str::FromStr;
7
8use super::ValidationError;
9
10/// A validated NNTP message ID (RFC 3977 ยง3.6)
11///
12/// Message IDs must be enclosed in angle brackets.
13/// Uses `Cow<'a, str>` for zero-copy parsing (borrowed) and owned storage.
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub struct MessageId<'a>(Cow<'a, str>);
16
17impl<'a> MessageId<'a> {
18    /// Create owned `MessageId` from String with validation
19    ///
20    /// # Errors
21    /// Returns `ValidationError::InvalidMessageId` when the string is not a
22    /// valid RFC-style NNTP message ID.
23    pub fn new(s: String) -> Result<Self, ValidationError> {
24        Self::validate(&s)?;
25        Ok(Self(Cow::Owned(s)))
26    }
27
28    /// Create borrowed `MessageId` from &str (zero-copy)
29    #[inline]
30    ///
31    /// # Errors
32    /// Returns `ValidationError::InvalidMessageId` when the string is not a
33    /// valid RFC-style NNTP message ID.
34    pub fn from_borrowed(s: &'a str) -> Result<Self, ValidationError> {
35        Self::validate(s)?;
36        Ok(Self(Cow::Borrowed(s)))
37    }
38
39    /// Create from pre-validated string (zero-copy, unchecked)
40    ///
41    /// # Safety
42    /// Caller must ensure: `s.len() >= 3`, `s.starts_with('<')`, `s.ends_with('>')`
43    #[inline]
44    #[must_use]
45    pub const unsafe fn from_str_unchecked(s: &'a str) -> Self {
46        Self(Cow::Borrowed(s))
47    }
48
49    /// Create owned `MessageId`, auto-wrapping in angle brackets if needed
50    ///
51    /// # Errors
52    /// Returns `ValidationError::InvalidMessageId` if the resulting wrapped
53    /// value is still not a valid message ID.
54    pub fn from_str_or_wrap(s: impl AsRef<str>) -> Result<MessageId<'static>, ValidationError> {
55        let s = s.as_ref();
56        if s.is_empty() {
57            return Err(ValidationError::InvalidMessageId("empty".to_string()));
58        }
59        let wrapped = if s.starts_with('<') && s.ends_with('>') {
60            s.to_string()
61        } else {
62            format!("<{s}>")
63        };
64        MessageId::new(wrapped)
65    }
66
67    #[inline]
68    fn validate(s: &str) -> Result<(), ValidationError> {
69        if s.len() < 3 || !s.starts_with('<') || !s.ends_with('>') {
70            Err(ValidationError::InvalidMessageId(
71                "must be <...>".to_string(),
72            ))
73        } else {
74            Ok(())
75        }
76    }
77
78    #[must_use]
79    #[inline]
80    pub fn as_str(&self) -> &str {
81        &self.0
82    }
83
84    #[must_use]
85    #[inline]
86    pub fn without_brackets(&self) -> &str {
87        &self.0[1..self.0.len() - 1]
88    }
89
90    #[must_use]
91    pub fn into_owned(self) -> MessageId<'static> {
92        MessageId(Cow::Owned(self.0.into_owned()))
93    }
94
95    #[must_use]
96    pub fn to_owned(&self) -> MessageId<'static> {
97        MessageId(Cow::Owned(self.0.clone().into_owned()))
98    }
99}
100
101impl FromStr for MessageId<'static> {
102    type Err = ValidationError;
103    fn from_str(s: &str) -> Result<Self, Self::Err> {
104        MessageId::new(s.to_string())
105    }
106}
107
108impl AsRef<str> for MessageId<'_> {
109    #[inline]
110    fn as_ref(&self) -> &str {
111        &self.0
112    }
113}
114
115impl std::ops::Deref for MessageId<'_> {
116    type Target = str;
117    #[inline]
118    fn deref(&self) -> &Self::Target {
119        &self.0
120    }
121}
122
123impl Borrow<str> for MessageId<'_> {
124    fn borrow(&self) -> &str {
125        &self.0
126    }
127}
128
129impl fmt::Display for MessageId<'_> {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        f.write_str(&self.0)
132    }
133}
134
135impl TryFrom<String> for MessageId<'static> {
136    type Error = ValidationError;
137    fn try_from(s: String) -> Result<Self, Self::Error> {
138        MessageId::new(s)
139    }
140}
141
142impl<'a> From<MessageId<'a>> for String {
143    fn from(msgid: MessageId<'a>) -> Self {
144        msgid.0.into_owned()
145    }
146}
147
148impl Serialize for MessageId<'_> {
149    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
150    where
151        S: serde::Serializer,
152    {
153        serializer.serialize_str(&self.0)
154    }
155}
156
157impl<'de> Deserialize<'de> for MessageId<'static> {
158    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159    where
160        D: serde::Deserializer<'de>,
161    {
162        let s = String::deserialize(deserializer)?;
163        MessageId::new(s).map_err(serde::de::Error::custom)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_message_id_validation() {
173        assert!(MessageId::new("<12345@example.com>".to_string()).is_ok());
174        assert!(MessageId::new("missing-brackets".to_string()).is_err());
175        assert!(MessageId::new("<>".to_string()).is_err());
176        assert!(MessageId::new(String::new()).is_err());
177    }
178
179    #[test]
180    fn test_message_id_without_brackets() {
181        let msgid = MessageId::new("<test@example.com>".to_string()).unwrap();
182        assert_eq!(msgid.without_brackets(), "test@example.com");
183    }
184
185    #[test]
186    fn test_from_str_or_wrap() {
187        assert_eq!(
188            MessageId::from_str_or_wrap("<test@example.com>")
189                .unwrap()
190                .as_str(),
191            "<test@example.com>"
192        );
193        assert_eq!(
194            MessageId::from_str_or_wrap("test@example.com")
195                .unwrap()
196                .as_str(),
197            "<test@example.com>"
198        );
199        // Empty string should error
200        assert!(MessageId::from_str_or_wrap("").is_err());
201    }
202
203    #[test]
204    fn test_from_borrowed() {
205        let s = "<borrowed@example.com>";
206        let msgid = MessageId::from_borrowed(s).unwrap();
207        assert_eq!(msgid.as_str(), s);
208
209        // Invalid borrowed should error
210        assert!(MessageId::from_borrowed("no-brackets").is_err());
211        assert!(MessageId::from_borrowed("<>").is_err());
212    }
213
214    #[test]
215    fn test_as_str() {
216        let msgid = MessageId::new("<test@example.com>".to_string()).unwrap();
217        assert_eq!(msgid.as_str(), "<test@example.com>");
218    }
219
220    #[test]
221    fn test_into_owned() {
222        let s = "<borrowed@example.com>";
223        let msgid = MessageId::from_borrowed(s).unwrap();
224        let owned = msgid.into_owned();
225        assert_eq!(owned.as_str(), s);
226    }
227
228    #[test]
229    fn test_to_owned() {
230        let s = "<borrowed@example.com>";
231        let msgid = MessageId::from_borrowed(s).unwrap();
232        let owned = msgid.to_owned();
233        assert_eq!(owned.as_str(), s);
234        // Original still valid
235        assert_eq!(msgid.as_str(), s);
236    }
237
238    #[test]
239    fn test_from_str() {
240        let msgid: MessageId = "<test@example.com>".parse().unwrap();
241        assert_eq!(msgid.as_str(), "<test@example.com>");
242
243        assert!("invalid".parse::<MessageId>().is_err());
244    }
245
246    #[test]
247    fn test_try_from_string() {
248        let msgid = MessageId::try_from("<test@example.com>".to_string()).unwrap();
249        assert_eq!(msgid.as_str(), "<test@example.com>");
250
251        assert!(MessageId::try_from("invalid".to_string()).is_err());
252    }
253
254    #[test]
255    fn test_into_string() {
256        let msgid = MessageId::new("<test@example.com>".to_string()).unwrap();
257        let s: String = msgid.into();
258        assert_eq!(s, "<test@example.com>");
259    }
260
261    #[test]
262    fn test_validation_edge_cases() {
263        // Too short (len < 3)
264        assert!(MessageId::new("<>".to_string()).is_err());
265        assert!(MessageId::new("<a".to_string()).is_err());
266        assert!(MessageId::new("a>".to_string()).is_err());
267
268        // Missing brackets
269        assert!(MessageId::new("<no-end".to_string()).is_err());
270        assert!(MessageId::new("no-start>".to_string()).is_err());
271
272        // Valid minimal
273        assert!(MessageId::new("<a>".to_string()).is_ok());
274    }
275}