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#[doc(alias = "msgid")]
29#[doc(alias = "article_id")]
30#[doc(alias = "message_identifier")]
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub struct MessageId<'a>(Cow<'a, str>);
33
34impl<'a> MessageId<'a> {
35 pub fn new(s: String) -> Result<Self, ValidationError> {
48 if s.len() < 3 {
49 return Err(ValidationError::InvalidMessageId(
50 "Message ID too short (minimum 3 characters)".to_string(),
51 ));
52 }
53 if !s.starts_with('<') || !s.ends_with('>') {
54 return Err(ValidationError::InvalidMessageId(
55 "Message ID must be enclosed in angle brackets".to_string(),
56 ));
57 }
58 Ok(Self(Cow::Owned(s)))
59 }
60
61 #[inline]
78 pub fn from_borrowed(s: &'a str) -> Result<Self, ValidationError> {
79 if s.len() < 3 {
80 return Err(ValidationError::InvalidMessageId(
81 "Message ID too short (minimum 3 characters)".to_string(),
82 ));
83 }
84 if !s.starts_with('<') || !s.ends_with('>') {
85 return Err(ValidationError::InvalidMessageId(
86 "Message ID must be enclosed in angle brackets".to_string(),
87 ));
88 }
89 Ok(Self(Cow::Borrowed(s)))
90 }
91
92 #[inline(always)]
107 pub unsafe fn from_str_unchecked(s: &'a str) -> Self {
108 Self(Cow::Borrowed(s))
109 }
110
111 pub fn from_str_or_wrap(s: impl AsRef<str>) -> Result<MessageId<'static>, ValidationError> {
124 let s = s.as_ref();
125 if s.is_empty() {
126 return Err(ValidationError::InvalidMessageId(
127 "Message ID cannot be empty".to_string(),
128 ));
129 }
130
131 let wrapped = if s.starts_with('<') && s.ends_with('>') {
132 s.to_string()
133 } else {
134 format!("<{}>", s)
135 };
136
137 MessageId::new(wrapped)
138 }
139
140 #[must_use]
142 #[inline]
143 pub fn as_str(&self) -> &str {
144 &self.0
145 }
146
147 #[must_use]
157 #[inline]
158 pub fn without_brackets(&self) -> &str {
159 let s: &str = &self.0;
160 &s[1..s.len() - 1]
161 }
162
163 pub fn extract_from_command(command: &str) -> Option<MessageId<'static>> {
176 let start = command.find('<')?;
177 let end = command[start..].find('>')?;
179 MessageId::new(command[start..=start + end].to_string()).ok()
181 }
182
183 pub fn into_owned(self) -> MessageId<'static> {
190 MessageId(Cow::Owned(self.0.into_owned()))
191 }
192
193 pub fn to_owned(&self) -> MessageId<'static> {
198 MessageId(Cow::Owned(self.0.clone().into_owned()))
199 }
200}
201
202impl FromStr for MessageId<'static> {
203 type Err = ValidationError;
204
205 fn from_str(s: &str) -> Result<Self, Self::Err> {
206 MessageId::new(s.to_string())
207 }
208}
209
210impl<'a> AsRef<str> for MessageId<'a> {
211 fn as_ref(&self) -> &str {
212 &self.0
213 }
214}
215
216impl<'a> std::ops::Deref for MessageId<'a> {
217 type Target = str;
218
219 #[inline]
220 fn deref(&self) -> &Self::Target {
221 &self.0
222 }
223}
224
225impl<'a> Borrow<str> for MessageId<'a> {
226 fn borrow(&self) -> &str {
227 &self.0
228 }
229}
230
231impl<'a> fmt::Display for MessageId<'a> {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 write!(f, "{}", self.0)
234 }
235}
236
237impl TryFrom<String> for MessageId<'static> {
238 type Error = ValidationError;
239
240 fn try_from(s: String) -> Result<Self, Self::Error> {
241 MessageId::new(s)
242 }
243}
244
245impl<'a> From<MessageId<'a>> for String {
246 fn from(msgid: MessageId<'a>) -> Self {
247 msgid.0.into_owned()
248 }
249}
250
251impl<'a> Serialize for MessageId<'a> {
252 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
253 where
254 S: serde::Serializer,
255 {
256 serializer.serialize_str(&self.0)
257 }
258}
259
260impl<'de> Deserialize<'de> for MessageId<'static> {
261 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
262 where
263 D: serde::Deserializer<'de>,
264 {
265 let s = String::deserialize(deserializer)?;
266 MessageId::new(s).map_err(serde::de::Error::custom)
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_valid_message_id() {
276 let msgid = MessageId::new("<12345@example.com>".to_string()).unwrap();
277 assert_eq!(msgid.as_str(), "<12345@example.com>");
278 assert_eq!(msgid.without_brackets(), "12345@example.com");
279 }
280
281 #[test]
282 fn test_complex_message_id() {
283 let msgid =
284 MessageId::new("<very-long-id.123.abc@subdomain.example.org>".to_string()).unwrap();
285 assert_eq!(
286 msgid.without_brackets(),
287 "very-long-id.123.abc@subdomain.example.org"
288 );
289 }
290
291 #[test]
292 fn test_message_id_without_brackets_rejected() {
293 let result = MessageId::new("12345@example.com".to_string());
294 assert!(result.is_err());
295 }
296
297 #[test]
298 fn test_message_id_missing_start_bracket() {
299 let result = MessageId::new("12345@example.com>".to_string());
300 assert!(result.is_err());
301 }
302
303 #[test]
304 fn test_message_id_missing_end_bracket() {
305 let result = MessageId::new("<12345@example.com".to_string());
306 assert!(result.is_err());
307 }
308
309 #[test]
310 fn test_empty_message_id() {
311 let result = MessageId::new("".to_string());
312 assert!(result.is_err());
313 }
314
315 #[test]
316 fn test_message_id_too_short() {
317 let result = MessageId::new("<>".to_string());
318 assert!(result.is_err());
319 }
320
321 #[test]
322 fn test_minimal_valid_message_id() {
323 let msgid = MessageId::new("<x>".to_string()).unwrap();
324 assert_eq!(msgid.without_brackets(), "x");
325 }
326
327 #[test]
328 fn test_from_str_or_wrap_with_brackets() {
329 let msgid = MessageId::from_str_or_wrap("<12345@example.com>").unwrap();
330 assert_eq!(msgid.as_str(), "<12345@example.com>");
331 }
332
333 #[test]
334 fn test_from_str_or_wrap_without_brackets() {
335 let msgid = MessageId::from_str_or_wrap("12345@example.com").unwrap();
336 assert_eq!(msgid.as_str(), "<12345@example.com>");
337 }
338
339 #[test]
340 fn test_from_str_or_wrap_empty() {
341 let result = MessageId::from_str_or_wrap("");
342 assert!(result.is_err());
343 }
344
345 #[test]
346 fn test_extract_from_command() {
347 let msgid = MessageId::extract_from_command("ARTICLE <12345@example.com>").unwrap();
348 assert_eq!(msgid.as_str(), "<12345@example.com>");
349 }
350
351 #[test]
352 fn test_extract_from_command_body() {
353 let msgid = MessageId::extract_from_command("BODY <test@news.server.com>").unwrap();
354 assert_eq!(msgid.as_str(), "<test@news.server.com>");
355 }
356
357 #[test]
358 fn test_extract_from_command_with_extra_text() {
359 let msgid =
360 MessageId::extract_from_command("ARTICLE <12345@example.com> extra text").unwrap();
361 assert_eq!(msgid.as_str(), "<12345@example.com>");
362 }
363
364 #[test]
365 fn test_extract_from_command_no_message_id() {
366 let result = MessageId::extract_from_command("LIST");
367 assert!(result.is_none());
368 }
369
370 #[test]
371 fn test_extract_from_command_malformed() {
372 let result = MessageId::extract_from_command("ARTICLE >12345@example.com<");
373 assert!(result.is_none());
374 }
375
376 #[test]
377 fn test_display() {
378 let msgid = MessageId::new("<12345@example.com>".to_string()).unwrap();
379 assert_eq!(format!("{}", msgid), "<12345@example.com>");
380 }
381
382 #[test]
383 fn test_as_ref() {
384 let msgid = MessageId::new("<12345@example.com>".to_string()).unwrap();
385 let s: &str = msgid.as_ref();
386 assert_eq!(s, "<12345@example.com>");
387 }
388
389 #[test]
390 fn test_try_from_string() {
391 let msgid: MessageId = "<12345@example.com>".to_string().try_into().unwrap();
392 assert_eq!(msgid.as_str(), "<12345@example.com>");
393 }
394
395 #[test]
396 fn test_into_string() {
397 let msgid = MessageId::new("<12345@example.com>".to_string()).unwrap();
398 let s: String = msgid.into();
399 assert_eq!(s, "<12345@example.com>");
400 }
401
402 #[test]
403 fn test_clone() {
404 let msgid = MessageId::new("<12345@example.com>".to_string()).unwrap();
405 let cloned = msgid.clone();
406 assert_eq!(msgid, cloned);
407 }
408
409 #[test]
410 fn test_equality() {
411 let msgid1 = MessageId::new("<12345@example.com>".to_string()).unwrap();
412 let msgid2 = MessageId::new("<12345@example.com>".to_string()).unwrap();
413 let msgid3 = MessageId::new("<54321@example.com>".to_string()).unwrap();
414 assert_eq!(msgid1, msgid2);
415 assert_ne!(msgid1, msgid3);
416 }
417
418 #[test]
419 fn test_serde_roundtrip() {
420 let msgid = MessageId::new("<12345@example.com>".to_string()).unwrap();
421 let json = serde_json::to_string(&msgid).unwrap();
422 assert_eq!(json, "\"<12345@example.com>\"");
423
424 let deserialized: MessageId = serde_json::from_str(&json).unwrap();
425 assert_eq!(deserialized, msgid);
426 }
427
428 #[test]
429 fn test_serde_invalid() {
430 let json = "\"invalid-msgid\"";
431 let result: Result<MessageId, _> = serde_json::from_str(json);
432 assert!(result.is_err());
433 }
434}