Skip to main content

rews/
article.rs

1use std::{
2  convert::Infallible,
3  fmt::Display,
4  num::ParseIntError,
5  str::{from_utf8, FromStr},
6};
7
8use itertools::Itertools;
9use serde::{Deserialize, Serialize};
10
11use crate::connection::{Status, StatusCode};
12
13macro_rules! regex {
14  ($re:literal $(,)?) => {{
15    static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new();
16    RE.get_or_init(|| regex::Regex::new($re).unwrap())
17  }};
18}
19
20#[derive(Clone, Debug)]
21pub struct Article {
22  pub headers: Headers,
23  pub body: Body,
24}
25
26impl Article {
27  pub fn from_u8(bytes: &[u8]) -> Result<Self, ArticleParseError> {
28    if let Some((headers, body)) = split_text(bytes) {
29      Ok(Article {
30        headers: Headers::from_u8(headers)?,
31        body: Body::new(body.to_vec()),
32      })
33    } else {
34      Err(ArticleParseError)
35    }
36  }
37}
38
39fn split_text(bytes: &[u8]) -> Option<(&[u8], &[u8])> {
40  for n in 0..bytes.len() - 4 {
41    if bytes[n..n + 4] == [0xd, 0xa, 0xd, 0xa] {
42      let headers = &bytes[0..n];
43      let body = &bytes[n + 4..];
44      return Some((headers, body));
45    }
46  }
47
48  None
49}
50
51pub struct ArticleParseError;
52
53#[derive(Clone, Debug)]
54pub struct Headers(Vec<Header>);
55
56impl Headers {
57  pub fn from_u8(bytes: &[u8]) -> Result<Headers, ArticleParseError> {
58    from_utf8(bytes)
59      .map_err(|_| ArticleParseError)
60      .and_then(|text| text.parse::<Headers>().map_err(|_| ArticleParseError))
61  }
62}
63
64impl FromStr for Headers {
65  type Err = Vec<HeaderParseError>;
66
67  fn from_str(s: &str) -> Result<Self, Self::Err> {
68    let (successes, failures): (Vec<Header>, Vec<HeaderParseError>) =
69      s.lines().map(Header::from_str).partition_result();
70
71    if failures.is_empty() {
72      Ok(Headers(successes))
73    } else {
74      Err(failures)
75    }
76  }
77}
78
79#[derive(Clone, Debug)]
80pub struct Header {
81  pub key: String,
82  pub value: String,
83}
84
85impl FromStr for Header {
86  type Err = HeaderParseError;
87
88  fn from_str(s: &str) -> Result<Self, Self::Err> {
89    let mut header = s.splitn(2, ':');
90    header
91      .next()
92      .and_then(|key| {
93        header.next().map(|val| Header {
94          key: key.trim().to_string(),
95          value: val.trim().to_string(),
96        })
97      })
98      .ok_or_else(|| HeaderParseError(s.to_string()))
99  }
100}
101
102pub struct HeaderParseError(String);
103
104#[derive(Clone, Debug)]
105pub struct Body(Vec<u8>);
106
107impl Body {
108  pub fn new(data: Vec<u8>) -> Self {
109    Body(data)
110  }
111
112  pub fn bytes(&self) -> Vec<u8> {
113    self.0.to_vec()
114  }
115}
116
117#[derive(Debug, PartialEq, Eq)]
118pub struct ArticleStatus {
119  pub status_code: StatusCode,
120  pub article_number: ArticleNumber,
121  pub message_id: MessageId,
122  pub message: String,
123}
124
125impl TryFrom<Status> for ArticleStatus {
126  type Error = ArticleStatusParseError;
127
128  fn try_from(status: Status) -> Result<Self, Self::Error> {
129    regex!(r"^(?P<article_number>\d+) <(?P<message_id>.+)> (?P<message>.+)$")
130      .captures(&status.message)
131      .and_then(|caps| {
132        let article_number = caps
133          .name("article_number")?
134          .as_str()
135          .parse::<ArticleNumber>()
136          .ok()?;
137        let message_id = caps
138          .name("message_id")?
139          .as_str()
140          .parse::<MessageId>()
141          .ok()?;
142        let message = caps.name("message")?.as_str().to_string();
143
144        Some(ArticleStatus {
145          status_code: status.status_code,
146          article_number,
147          message_id,
148          message,
149        })
150      })
151      .ok_or(ArticleStatusParseError)
152  }
153}
154
155#[derive(Debug, PartialEq, Eq)]
156pub struct ArticleStatusParseError;
157
158#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash, Debug)]
159pub struct ArticleNumber(usize);
160
161impl From<usize> for ArticleNumber {
162  fn from(val: usize) -> Self {
163    Self(val)
164  }
165}
166
167impl FromStr for ArticleNumber {
168  type Err = ParseIntError;
169
170  fn from_str(s: &str) -> Result<Self, Self::Err> {
171    s.parse().map(Self)
172  }
173}
174
175#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug, Deserialize, Serialize)]
176pub struct MessageId(String);
177
178impl FromStr for MessageId {
179  type Err = Infallible;
180
181  fn from_str(s: &str) -> Result<Self, Self::Err> {
182    String::from_str(s).map(Self)
183  }
184}
185
186impl Display for MessageId {
187  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188    self.0.fmt(f)
189  }
190}
191
192#[cfg(test)]
193mod tests {
194  use std::str::FromStr;
195
196  use crate::{
197    article::{split_text, ArticleNumber, ArticleStatus, ArticleStatusParseError, MessageId},
198    connection::{Status, StatusCode},
199  };
200
201  #[test]
202  fn split_text_with_delimiter() {
203    let result: Option<(&[u8], &[u8])> = split_text(b"a\r\n\r\nb");
204    let expected: Option<(&[u8], &[u8])> = Some((b"a", b"b"));
205    assert_eq!(result, expected);
206  }
207
208  #[test]
209  fn split_text_without_delimiter() {
210    let result: Option<(&[u8], &[u8])> = split_text(b"a\r\r\nb");
211    assert_eq!(result, None);
212  }
213
214  #[test]
215  fn parse_article_status() {
216    let result = ArticleStatus::try_from(Status {
217      status_code: StatusCode::from(220),
218      message: "2 <message.002> article exists".to_string(),
219    });
220    assert_eq!(
221      result,
222      Ok(ArticleStatus {
223        status_code: StatusCode::from(220),
224        article_number: ArticleNumber::from(2),
225        message_id: MessageId::from_str("message.002").unwrap(),
226        message: "article exists".to_string()
227      })
228    );
229  }
230
231  #[test]
232  fn parse_article_status_with_angle_brackets_message() {
233    let result = ArticleStatus::try_from(Status {
234      status_code: StatusCode::from(220),
235      message: "2 <message.002> <article exists>".to_string(),
236    });
237    assert_eq!(
238      result,
239      Ok(ArticleStatus {
240        status_code: StatusCode::from(220),
241        article_number: ArticleNumber::from(2),
242        message_id: MessageId::from_str("message.002").unwrap(),
243        message: "<article exists>".to_string()
244      })
245    );
246  }
247
248  #[test]
249  fn parse_article_status_without_angle_brackets() {
250    let result = ArticleStatus::try_from(Status {
251      status_code: StatusCode::from(220),
252      message: "2 message.002 article exists".to_string(),
253    });
254    assert_eq!(result, Err(ArticleStatusParseError));
255  }
256
257  #[test]
258  fn parse_article_status_without_message() {
259    let result = ArticleStatus::try_from(Status {
260      status_code: StatusCode::from(220),
261      message: "2 <message.002>".to_string(),
262    });
263    assert_eq!(result, Err(ArticleStatusParseError));
264  }
265
266  #[test]
267  fn parse_article_status_without_article_number() {
268    let result = ArticleStatus::try_from(Status {
269      status_code: StatusCode::from(220),
270      message: "<message.002> article exists".to_string(),
271    });
272    assert_eq!(result, Err(ArticleStatusParseError));
273  }
274
275  #[test]
276  fn parse_article_status_without_non_integer_article_number() {
277    let result = ArticleStatus::try_from(Status {
278      status_code: StatusCode::from(220),
279      message: "2.3 <message.002> article exists".to_string(),
280    });
281    assert_eq!(result, Err(ArticleStatusParseError));
282  }
283}