use std::{
convert::Infallible,
fmt::Display,
num::ParseIntError,
str::{from_utf8, FromStr},
};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use crate::connection::{Status, StatusCode};
macro_rules! regex {
($re:literal $(,)?) => {{
static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new();
RE.get_or_init(|| regex::Regex::new($re).unwrap())
}};
}
#[derive(Clone, Debug)]
pub struct Article {
pub headers: Headers,
pub body: Body,
}
impl Article {
pub fn from_u8(bytes: &[u8]) -> Result<Self, ArticleParseError> {
if let Some((headers, body)) = split_text(bytes) {
Ok(Article {
headers: Headers::from_u8(headers)?,
body: Body::new(body.to_vec()),
})
} else {
Err(ArticleParseError)
}
}
}
fn split_text(bytes: &[u8]) -> Option<(&[u8], &[u8])> {
for n in 0..bytes.len() - 4 {
if bytes[n..n + 4] == [0xd, 0xa, 0xd, 0xa] {
let headers = &bytes[0..n];
let body = &bytes[n + 4..];
return Some((headers, body));
}
}
None
}
pub struct ArticleParseError;
#[derive(Clone, Debug)]
pub struct Headers(Vec<Header>);
impl Headers {
pub fn from_u8(bytes: &[u8]) -> Result<Headers, ArticleParseError> {
from_utf8(bytes)
.map_err(|_| ArticleParseError)
.and_then(|text| text.parse::<Headers>().map_err(|_| ArticleParseError))
}
}
impl FromStr for Headers {
type Err = Vec<HeaderParseError>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (successes, failures): (Vec<Header>, Vec<HeaderParseError>) =
s.lines().map(Header::from_str).partition_result();
if failures.is_empty() {
Ok(Headers(successes))
} else {
Err(failures)
}
}
}
#[derive(Clone, Debug)]
pub struct Header {
pub key: String,
pub value: String,
}
impl FromStr for Header {
type Err = HeaderParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut header = s.splitn(2, ':');
header
.next()
.and_then(|key| {
header.next().map(|val| Header {
key: key.trim().to_string(),
value: val.trim().to_string(),
})
})
.ok_or_else(|| HeaderParseError(s.to_string()))
}
}
pub struct HeaderParseError(String);
#[derive(Clone, Debug)]
pub struct Body(Vec<u8>);
impl Body {
pub fn new(data: Vec<u8>) -> Self {
Body(data)
}
pub fn bytes(&self) -> Vec<u8> {
self.0.to_vec()
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct ArticleStatus {
pub status_code: StatusCode,
pub article_number: ArticleNumber,
pub message_id: MessageId,
pub message: String,
}
impl TryFrom<Status> for ArticleStatus {
type Error = ArticleStatusParseError;
fn try_from(status: Status) -> Result<Self, Self::Error> {
regex!(r"^(?P<article_number>\d+) <(?P<message_id>.+)> (?P<message>.+)$")
.captures(&status.message)
.and_then(|caps| {
let article_number = caps
.name("article_number")?
.as_str()
.parse::<ArticleNumber>()
.ok()?;
let message_id = caps
.name("message_id")?
.as_str()
.parse::<MessageId>()
.ok()?;
let message = caps.name("message")?.as_str().to_string();
Some(ArticleStatus {
status_code: status.status_code,
article_number,
message_id,
message,
})
})
.ok_or(ArticleStatusParseError)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct ArticleStatusParseError;
#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash, Debug)]
pub struct ArticleNumber(usize);
impl From<usize> for ArticleNumber {
fn from(val: usize) -> Self {
Self(val)
}
}
impl FromStr for ArticleNumber {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse().map(Self)
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug, Deserialize, Serialize)]
pub struct MessageId(String);
impl FromStr for MessageId {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
String::from_str(s).map(Self)
}
}
impl Display for MessageId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::{
article::{split_text, ArticleNumber, ArticleStatus, ArticleStatusParseError, MessageId},
connection::{Status, StatusCode},
};
#[test]
fn split_text_with_delimiter() {
let result: Option<(&[u8], &[u8])> = split_text(b"a\r\n\r\nb");
let expected: Option<(&[u8], &[u8])> = Some((b"a", b"b"));
assert_eq!(result, expected);
}
#[test]
fn split_text_without_delimiter() {
let result: Option<(&[u8], &[u8])> = split_text(b"a\r\r\nb");
assert_eq!(result, None);
}
#[test]
fn parse_article_status() {
let result = ArticleStatus::try_from(Status {
status_code: StatusCode::from(220),
message: "2 <message.002> article exists".to_string(),
});
assert_eq!(
result,
Ok(ArticleStatus {
status_code: StatusCode::from(220),
article_number: ArticleNumber::from(2),
message_id: MessageId::from_str("message.002").unwrap(),
message: "article exists".to_string()
})
);
}
#[test]
fn parse_article_status_with_angle_brackets_message() {
let result = ArticleStatus::try_from(Status {
status_code: StatusCode::from(220),
message: "2 <message.002> <article exists>".to_string(),
});
assert_eq!(
result,
Ok(ArticleStatus {
status_code: StatusCode::from(220),
article_number: ArticleNumber::from(2),
message_id: MessageId::from_str("message.002").unwrap(),
message: "<article exists>".to_string()
})
);
}
#[test]
fn parse_article_status_without_angle_brackets() {
let result = ArticleStatus::try_from(Status {
status_code: StatusCode::from(220),
message: "2 message.002 article exists".to_string(),
});
assert_eq!(result, Err(ArticleStatusParseError));
}
#[test]
fn parse_article_status_without_message() {
let result = ArticleStatus::try_from(Status {
status_code: StatusCode::from(220),
message: "2 <message.002>".to_string(),
});
assert_eq!(result, Err(ArticleStatusParseError));
}
#[test]
fn parse_article_status_without_article_number() {
let result = ArticleStatus::try_from(Status {
status_code: StatusCode::from(220),
message: "<message.002> article exists".to_string(),
});
assert_eq!(result, Err(ArticleStatusParseError));
}
#[test]
fn parse_article_status_without_non_integer_article_number() {
let result = ArticleStatus::try_from(Status {
status_code: StatusCode::from(220),
message: "2.3 <message.002> article exists".to_string(),
});
assert_eq!(result, Err(ArticleStatusParseError));
}
}