#![deny(clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
mod primitives;
mod replies;
pub use crate::primitives::*;
pub use crate::replies::{Reply, ServerMessage, Source};
use bytes::Bytes;
use nom::{
bytes::complete::{tag, take_till},
error::Error as NomError,
};
use nom_bytes::BytesWrapper;
fn parse_optional_source(input: BytesWrapper) -> nom::IResult<BytesWrapper, BytesWrapper> {
let (rest, _) = tag(":".as_bytes())(input)?;
let (rest, _) = take_till(|c| c == b' ')(rest)?;
tag(" ".as_bytes())(rest)
}
macro_rules! define_commands {
(
$(
$name:ident$((
$($param:ident$(<$($gen:tt),+>)?),*
))?
),* $(,)?
) => {
paste::paste! {
#[derive(Debug)]
pub enum Command<'a> {
$([<$name:camel>]([<$name:camel Command>]<'a>)),*
}
$(const [<$name _BYTES>]: &[u8] = stringify!($name).as_bytes();)*
impl Command<'_> {
pub fn parse(input: Bytes) -> Result<Option<Self>, nom::Err<NomError<BytesWrapper>>> {
let input = BytesWrapper::from(input);
let input = if let Ok((input, _)) = parse_optional_source(input.clone()) {
input
} else {
input
};
let (params, command) = take_till(|c| c == b' ')(input)?;
match command.to_ascii_uppercase().as_ref() {
$([<$name _BYTES>] => Ok(Some(Self::[<$name:camel>]([<$name:camel Command>]::parse(params)?)))),*,
_ => Ok(None)
}
}
}
impl std::fmt::Display for Command<'_> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
$(Self::[<$name:camel>](cmd) => cmd.fmt(fmt)),*
}
}
}
$(
#[derive(Debug)]
pub struct [<$name:camel Command>]<'a> {
pub _phantom: std::marker::PhantomData<&'a ()>,
$($(pub [<$param:snake>]: $param$(<$($gen),+>)?),*),*
}
impl [<$name:camel Command>]<'_> {
#[allow(unused_variables)]
pub fn parse(rest: BytesWrapper) -> Result<Self, nom::Err<nom::error::Error<BytesWrapper>>> {
$(
$(
let (rest, _) = tag(" ".as_bytes())(rest)?;
let (rest, [<$param:snake>]) = $param::parse(rest)?;
)*
)*
Ok(Self {
_phantom: std::marker::PhantomData,
$($([<$param:snake>]),*),*
})
}
}
impl std::fmt::Display for [<$name:camel Command>]<'_> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt.write_str(stringify!($name))?;
$(
$(
fmt.write_str(" ")?;
self.[<$param:snake>].fmt(fmt)?;
)*
)*
Ok(())
}
}
impl<'a> Into<Command<'a>> for [<$name:camel Command>]<'a> {
fn into(self) -> Command<'a> {
Command::[<$name:camel>](self)
}
}
)*
}
};
}
define_commands! {
USER(Username<'a>, HostName<'a>, ServerName<'a>, RealName<'a>),
NICK(Nick<'a>),
MOTD,
VERSION,
HELP,
USERS,
TIME,
PONG(ServerName<'a>),
PING(ServerName<'a>),
LIST,
MODE(Nick<'a>, Mode<'a>),
WHOIS(Nick<'a>),
USERHOST(Nick<'a>),
USERIP(Nick<'a>),
JOIN(Channel<'a>),
PRIVMSG(Receiver<'a>, FreeText<'a>),
}
#[cfg(test)]
mod tests {
use super::Command;
use bytes::Bytes;
#[test]
fn parse_empty() {
assert!(matches!(Command::parse(Bytes::from_static(b"")), Ok(None)));
}
#[test]
fn parse_privmsg() {
assert!(matches!(
Command::parse(Bytes::from_static(b"PRIVMSG foo :baz")),
Ok(Some(Command::Privmsg(super::PrivmsgCommand {
receiver: super::Receiver::User(super::Nick(nick)),
free_text: super::primitives::FreeText(msg),
_phantom: std::marker::PhantomData,
}))) if &*nick == b"foo" && &*msg == b"baz"
))
}
#[test]
fn parse_privmsg_opt_source() {
assert!(matches!(
Command::parse(Bytes::from_static(b":some-fake-source!dude@nice PRIVMSG foo :baz")),
Ok(Some(Command::Privmsg(super::PrivmsgCommand {
receiver: super::Receiver::User(super::Nick(nick)),
free_text: super::primitives::FreeText(msg),
_phantom: std::marker::PhantomData,
}))) if &*nick == b"foo" && &*msg == b"baz"
))
}
}