yaircc 0.1.0

Yet Another low level IRC Client library
Documentation
//! Message parsing module from [sbstp/loirc](https://github.com/sbstp/loirc).
//! See COPYRIGHT for details.

use {
    crate::code::Code,
    std::{fmt, str::FromStr},
};

/// Error generated by the parser.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum ParseError {
    /// String was empty.
    EmptyCommand,
    /// Message did not have a code.
    EmptyMessage,
    /// Unexpected end of the string.
    UnexpectedEnd,
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{}",
            match *self {
                ParseError::EmptyCommand => "String was empty",
                ParseError::EmptyMessage => "Message did not have a code",
                ParseError::UnexpectedEnd => "Unexpected end of the string",
            }
        )
    }
}

impl std::error::Error for ParseError {}

/// Represents a message received from the server.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Message {
    /// Prefix
    pub prefix: Option<Prefix>,
    /// Code
    pub code: Code,
    /// Arguments
    pub args: Vec<String>,
}

impl Message {
    /// Parse the given string into a `Message` struct.
    ///
    /// An error is returned if the message is not valid.
    pub fn parse(line: &str) -> Result<Message, ParseError> {
        if line.is_empty() || line.trim().is_empty() {
            return Err(ParseError::EmptyMessage);
        }

        let mut state = line.trim_end_matches("\r\n");
        let mut prefix: Option<Prefix> = None;
        let code: Option<&str>;
        let mut args: Vec<String> = Vec::new();

        // Look for a prefix
        if state.starts_with(':') {
            match state.find(' ') {
                None => return Err(ParseError::UnexpectedEnd),
                Some(idx) => {
                    prefix = parse_prefix(&state[1..idx]);
                    state = &state[idx + 1..];
                }
            }
        }

        // Look for the command/reply
        match state.find(' ') {
            None => {
                if state.is_empty() {
                    return Err(ParseError::EmptyMessage);
                } else {
                    code = Some(&state[..]);
                    state = &state[state.len()..];
                }
            }
            Some(idx) => {
                code = Some(state[..idx].into());
                state = &state[idx + 1..];
            }
        }

        // Look for arguments and the suffix
        if !state.is_empty() {
            loop {
                if state.starts_with(':') {
                    args.push(state[1..].into());
                    break;
                } else {
                    match state.find(' ') {
                        None => {
                            args.push(state[..].into());
                            break;
                        }
                        Some(idx) => {
                            args.push(state[..idx].into());
                            state = &state[idx + 1..];
                        }
                    }
                }
            }
        }

        let code = match code {
            None => return Err(ParseError::EmptyCommand),
            Some(text) => match text.parse() {
                Ok(code) => code,
                Err(_) => Code::Unknown(text.into()),
            },
        };

        Ok(Message { prefix, code, args })
    }
}

impl FromStr for Message {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Message::parse(s)
    }
}

fn parse_prefix(prefix: &str) -> Option<Prefix> {
    match prefix.find('!') {
        None => Some(Prefix::Server(prefix.to_string())),
        Some(excpos) => {
            let nick = &prefix[..excpos];
            let rest = &prefix[excpos + 1..];
            match rest.find('@') {
                None => None,
                Some(atpos) => {
                    let user = &rest[..atpos];
                    let host = &rest[atpos + 1..];
                    Some(Prefix::User(PrefixUser::new(nick, user, host)))
                }
            }
        }
    }
}

/// Prefix of the message.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Prefix {
    /// Prefix is a user.
    User(PrefixUser),
    /// Prefix is a server.
    Server(String),
}

/// User prefix representation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PrefixUser {
    /// Nickname
    pub nickname: String,
    /// Username
    pub username: String,
    /// Hostname
    pub hostname: String,
}

impl PrefixUser {
    fn new(nick: &str, user: &str, host: &str) -> PrefixUser {
        PrefixUser {
            nickname: nick.into(),
            username: user.into(),
            hostname: host.into(),
        }
    }
}

#[test]
fn test_full() {
    let res = Message::parse(":org.prefix.cool COMMAND arg1 arg2 arg3 :suffix is pretty cool yo");
    assert!(res.is_ok());
    let msg = res.ok().unwrap();
    assert_eq!(msg.code, Code::Unknown("COMMAND".to_string()));
    assert_eq!(
        msg.args,
        vec!["arg1", "arg2", "arg3", "suffix is pretty cool yo"]
    );
}

#[test]
fn test_no_prefix() {
    let res = Message::parse("NICK arg1 arg2 arg3 :suffix is pretty cool yo");
    assert!(res.is_ok());
    let msg = res.ok().unwrap();
    assert_eq!(msg.prefix, None);
    assert_eq!(msg.code, Code::Nick);
    assert_eq!(
        msg.args,
        vec!["arg1", "arg2", "arg3", "suffix is pretty cool yo"]
    );
}

#[test]
fn test_no_suffix() {
    let res = Message::parse(":org.prefix.cool NICK arg1 arg2 arg3");
    assert!(res.is_ok());
    let msg = res.ok().unwrap();
    assert_eq!(msg.code, Code::Nick);
    assert_eq!(msg.args, vec!["arg1", "arg2", "arg3"]);
}

#[test]
fn test_no_args() {
    let res = Message::parse(":org.prefix.cool NICK :suffix is pretty cool yo");
    assert!(res.is_ok());
    let msg = res.ok().unwrap();
    assert_eq!(msg.code, Code::Nick);
    assert_eq!(msg.args, vec!["suffix is pretty cool yo"]);
}

#[test]
fn test_only_command() {
    let res = Message::parse("NICK");
    assert!(res.is_ok());
    let msg = res.ok().unwrap();
    assert_eq!(msg.prefix, None);
    assert_eq!(msg.code, Code::Nick);
    assert_eq!(msg.args.len(), 0);
}

#[test]
fn test_empty_message() {
    let res = Message::parse("");
    assert!(res.is_err());
    let err = res.err().unwrap();
    assert!(err == ParseError::EmptyMessage);
}

#[test]
fn test_empty_message_trim() {
    let res = Message::parse("    ");
    assert!(res.is_err());
    let err = res.err().unwrap();
    assert!(err == ParseError::EmptyMessage);
}

#[test]
fn test_only_prefix() {
    let res = Message::parse(":org.prefix.cool");
    assert!(res.is_err());
    let err = res.err().unwrap();
    assert!(err == ParseError::UnexpectedEnd);
}

#[test]
fn test_prefix_none() {
    let res = Message::parse("COMMAND :suffix is pretty cool yo");
    assert!(res.is_ok());
    let msg = res.ok().unwrap();
    assert_eq!(msg.args, vec!["suffix is pretty cool yo"]);
}

#[test]
fn test_prefix_server() {
    let res = Message::parse(":irc.freenode.net COMMAND :suffix is pretty cool yo");
    assert!(res.is_ok());
    let msg = res.ok().unwrap();
    assert_eq!(msg.prefix, Some(Prefix::Server("irc.freenode.net".into())));
}

#[test]
fn test_prefix_user() {
    let res = Message::parse(":bob!bob@bob.com COMMAND :suffix is pretty cool yo");
    assert!(res.is_ok());
    let msg = res.ok().unwrap();
    assert_eq!(
        msg.prefix,
        Some(Prefix::User(PrefixUser::new("bob", "bob", "bob.com")))
    );
}