🏡 index : ~doyle/titanirc.git

#![deny(clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]

mod primitives;

pub use crate::primitives::*;

macro_rules! define_commands {
    (
        $(
            $name:ident$((
                $($param:ty),*
            ))?
        ),* $(,)?
    ) => {
        paste::paste! {
            #[derive(Debug)]
            pub enum Command {
                $([<$name:camel>]([<$name:camel Command>])),*
            }

            $(const [<$name _BYTES>]: &[u8] = stringify!($name).as_bytes();)*

            impl Command {
                pub fn parse(input: &[u8]) -> Result<Option<Self>, nom::Err<nom::error::Error<&[u8]>>> {
                    let (rest, kind) = nom::bytes::complete::take_till(|c| c == b' ')(input)?;

                    match kind {
                        $([<$name _BYTES>] => Ok(Some(Self::[<$name:camel>]([<$name:camel Command>]::parse(rest)?)))),*,
                        _ => Ok(None)
                    }
                }
            }

            $(
                #[derive(Debug)]
                pub struct [<$name:camel Command>] {
                    $($([<$param:snake>]: $param),*),*
                }

                impl [<$name:camel Command>] {
                    #[allow(unused_variables)]
                    pub fn parse(rest: &[u8]) -> Result<Self, nom::Err<nom::error::Error<&[u8]>>> {
                        $(
                            $(
                                let (rest, _) = nom::bytes::complete::tag(" ")(rest)?;
                                let (rest, [<$param:snake>]) = $param::parse(rest)?;
                            )*
                        )*

                        Ok(Self {
                            $($([<$param:snake>]),*),*
                        })
                    }
                }
            )*
        }
    };
}

define_commands! {
    USER(Username, HostName, ServerName, RealName),
    NICK(Nick),

    VERSION,
    HELP,
    USERS,
    TIME,
    LIST,
    WHOIS(Nick),
    USERHOST(Nick),
    USERIP(Nick),
    JOIN(Channel),

    PRIVMSG(Receiver, Message),
}

#[cfg(test)]
mod tests {
    use super::Command;

    #[test]
    fn parse_empty() {
        assert!(matches!(Command::parse(b""), Ok(None)));
    }

    #[test]
    fn parse_privmsg() {
        assert!(matches!(
            Command::parse(b"PRIVMSG foo :baz"),
            Ok(Some(Command::Privmsg(super::PrivmsgCommand {
                receiver: super::Receiver::User(super::Nick(nick)),
                message: super::Message(msg),
            }))) if nick == "foo" && msg == ":baz"
        ))
    }
}