use std::{str::{Chars, FromStr}, iter::Peekable, ops::Range, fmt}; use strum_macros::{EnumString, Display}; #[allow(clippy::upper_case_acronyms)] #[derive(Clone, Copy, Debug, PartialEq, Eq, EnumString, Display)] pub enum Command { PASS, SERVER, NICK, NJOIN, PING, PONG, MODE, JOIN, PRIVMSG, ERROR, #[strum(disabled)] Number(u16), } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Message<'a> { pub prefix: Option<&'a str>, pub command: Command, pub params: Vec<&'a str>, } impl<'a> Message<'a> { pub fn new(command: Command, params: Vec<&'a str>) -> Self { Self { prefix: None, command, params, } } pub fn with_prefix(prefix: &'a str, command: Command, params: Vec<&'a str>) -> Self { Self { prefix: Some(prefix), command, params, } } } impl<'a> fmt::Display for Message<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(prefix) = self.prefix { write!(f, ":{} ", prefix)?; } write!(f, "{}", self.command)?; let paramc = self.params.len(); if paramc > 0 { for param in &self.params[..(paramc-1)] { write!(f, " {}", param)?; } write!(f, " :{}", &self.params[paramc-1])?; } Ok(()) } } #[derive(Debug)] pub struct ParseError { msg: String, span: Range, } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}: {}", self.span, self.msg) } } impl std::error::Error for ParseError {} struct Parser<'a> { src: &'a str, chars: Peekable>, pos: usize, } impl<'a> Parser<'a> { fn next(&mut self) -> Option { self.chars .next() .map(|c| { self.pos += 1; c }) } fn peek(&mut self) -> Option { self.chars.peek().copied() } fn read_word(&mut self) -> &'a str { let start = self.pos; while !matches!(self.peek(), Some(' ') | Some('\n') | Some('\r') | None) { self.next(); } &self.src[start..self.pos] } fn parse(&mut self) -> Result, ParseError> { let prefix = if let Some(':') = self.peek() { self.next(); let p = self.read_word(); match self.peek() { Some(' ') => { self.next(); }, Some(c) => return Err(ParseError { msg: format!("expected space, found '{c}'"), span: self.pos..(self.pos+1), }), None => return Err(ParseError { msg: "expected space, found EOF".to_owned(), span: self.pos..(self.pos+1) }) } Some(p) } else { None }; let command = self.parse_command()?; let mut params = Vec::new(); while self.peek().is_some() { params.push(self.parse_param()?); } Ok(Message { prefix, command, params, }) } fn parse_param(&mut self) -> Result<&'a str, ParseError> { let Some(' ') = self.peek() else { let pos = self.pos; return Err(ParseError { msg: format!("expected space, found '{}'", self.next().unwrap()), span: pos..(pos+1), }) }; self.next(); if let Some(':') = self.peek() { self.next(); let start = self.pos; while self.next().is_some() {} Ok(&self.src[start..]) } else { let start = self.pos; while !matches!(self.peek(), Some(' ') | Some('\n') | Some('\r') | None) { self.next(); } Ok(&self.src[start..self.pos]) } } fn parse_command(&mut self) -> Result { let start = self.pos; let name = self.read_word(); if name.len() == 3 { if let Ok(n) = name.parse::() { return Ok(Command::Number(n)) } } Command::from_str(name) .map_err(|_| ParseError { msg: format!("unknown command: {name}"), span: start..self.pos }) } } pub fn parse(msg: &str) -> Result { let mut p = Parser { src: msg, chars: msg.chars().peekable(), pos: 0, }; p.parse() } #[cfg(test)] mod tests { use super::{parse, Message, Command, ParseError}; #[test] fn test() -> Result<(), ParseError> { assert_eq!( parse(":example.org PONG irc.example.com :irc.example.com")?, Message { prefix: Some("example.org"), command: Command::PONG, params: vec!["irc.example.com", "irc.example.com"], } ); assert_eq!( parse("PASS AAAABBBB 0210-IRC+ ngIRCd|26.1:CHLMSXZ PZ")?, Message { prefix: None, command: Command::PASS, params: vec!["AAAABBBB", "0210-IRC+", "ngIRCd|26.1:CHLMSXZ", "PZ"], } ); assert_eq!( parse(":example.org SERVER example.org 1 :Server Info Text")?, Message { prefix: Some("example.org"), command: Command::SERVER, params: vec!["example.org", "1", "Server Info Text"], } ); Ok(()) } }