rews 0.4.5

A binary client for Usenet.
Documentation
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));
  }
}