nntp_proxy/types/
protocol.rs1use serde::{Deserialize, Serialize};
4use std::borrow::{Borrow, Cow};
5use std::fmt;
6use std::str::FromStr;
7
8use super::ValidationError;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub struct MessageId<'a>(Cow<'a, str>);
16
17impl<'a> MessageId<'a> {
18 pub fn new(s: String) -> Result<Self, ValidationError> {
24 Self::validate(&s)?;
25 Ok(Self(Cow::Owned(s)))
26 }
27
28 #[inline]
30 pub fn from_borrowed(s: &'a str) -> Result<Self, ValidationError> {
35 Self::validate(s)?;
36 Ok(Self(Cow::Borrowed(s)))
37 }
38
39 #[inline]
44 #[must_use]
45 pub const unsafe fn from_str_unchecked(s: &'a str) -> Self {
46 Self(Cow::Borrowed(s))
47 }
48
49 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 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 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 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 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 assert!(MessageId::new("<no-end".to_string()).is_err());
270 assert!(MessageId::new("no-start>".to_string()).is_err());
271
272 assert!(MessageId::new("<a>".to_string()).is_ok());
274 }
275}