From 80fe0fcfef6a02b70e259398f70f08c2f01085c8 Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Tue, 2 Aug 2022 01:12:11 +0100 Subject: [PATCH] Allow parse function to accept multiple lines --- src/lib.rs | 189 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 123 insertions(+), 66 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 163d9d4..6a0a1df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,8 +37,8 @@ //! ``` //! let msg = "@id=123;name=rick :nick!user@host.tmi.twitch.tv PRIVMSG #rickastley :Never gonna give you up!"; //! match ircparser::parse(msg) { -//! Ok(x) => { -//! let line = x; +//! Ok(mut x) => { +//! let line = x.pop_front().unwrap(); //! //! assert_eq!(&line.tags["id"], "123"); //! if line.source.is_some() { @@ -58,7 +58,7 @@ mod line; pub use line::Line; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; type ParseResult = Result; @@ -107,17 +107,20 @@ fn find_index(text: &str, char: char, start: usize) -> Option { /// Parses an IRC message. /// /// # Arguments -/// - `line` - The line you want to parse. +/// - `text` - The text you want to parse. This can comprise of multiple +/// lines. In this case, each line (separated by a newline character) +/// will be a separate element in the return value. /// /// # Returns -/// - [`Line`] - An instance representing a parsed line. +/// - [`VecDeque`] - A [`VecDeque`] of all parsed [`Line`]s. /// /// # Example /// ``` /// let msg = "@id=123;name=rick :nick!user@host.tmi.twitch.tv PRIVMSG #rickastley :Never gonna give you up!"; +/// /// match ircparser::parse(msg) { -/// Ok(x) => { -/// let line = x; +/// Ok(mut x) => { +/// let line = x.pop_front().unwrap(); /// /// assert_eq!(&line.tags["id"], "123"); /// if line.source.is_some() { @@ -133,51 +136,62 @@ fn find_index(text: &str, char: char, start: usize) -> Option { /// } /// }; /// ``` -pub fn parse(line: &str) -> ParseResult { - if line.is_empty() { - return Err(ParseError::new("line length cannot be 0")); - } +/// +/// # Notice +/// The behaviour of this function changed in v0.2.0. It can now accept +/// multiple lines at once, but as a consequence, now returns a +/// [`VecDeque`] of [`Line`] objects instead of a single [`Line`]. +pub fn parse(text: &str) -> ParseResult> { + let mut parsed_lines: VecDeque = VecDeque::new(); - let mut idx = 0; - let mut tags: HashMap = HashMap::new(); - let mut source: Option = None; - - // Parse tags component. - if line.starts_with('@') { - idx = line.find(' ').unwrap(); - - for part in Some(&line[1..idx]).unwrap().split(';') { - let kv: Vec<&str> = part.split('=').collect(); - tags.insert(kv[0].to_string(), kv[1].to_string()); + for line in text.replace("\r", "").split("\n") { + if line.is_empty() { + return Err(ParseError::new("line length cannot be 0")); } - idx += 1; - } + let mut idx = 0; + let mut tags: HashMap = HashMap::new(); + let mut source: Option = None; - // Parse source component. - if line.chars().nth(idx).unwrap() == ':' { + // Parse tags component. + if line.starts_with('@') { + idx = line.find(' ').unwrap(); + + for part in Some(&line[1..idx]).unwrap().split(';') { + let kv: Vec<&str> = part.split('=').collect(); + tags.insert(kv[0].to_string(), kv[1].to_string()); + } + + idx += 1; + } + + // Parse source component. + if line.chars().nth(idx).unwrap() == ':' { + let end_idx = find_index(line, ' ', idx).unwrap(); + source = Some(line[idx..end_idx].to_string()); + idx = end_idx + 1; + } + + // Parse command component. let end_idx = find_index(line, ' ', idx).unwrap(); - source = Some(line[idx..end_idx].to_string()); + let command = &line[idx..end_idx]; idx = end_idx + 1; + + let c_idx = match find_index(line, ':', idx) { + Some(x) => x - 1, + None => line.len(), + }; + + // Parse params component. + let mut params: Vec = line[idx..c_idx].split(' ').map(|x| x.to_string()).collect(); + if c_idx != line.len() { + params.push(line[c_idx + 2..].to_string()); + } + + parsed_lines.push_back(Line::new(tags, source, command, params)); } - // Parse command component. - let end_idx = find_index(line, ' ', idx).unwrap(); - let command = &line[idx..end_idx]; - idx = end_idx + 1; - - let c_idx = match find_index(line, ':', idx) { - Some(x) => x - 1, - None => line.len(), - }; - - // Parse params component. - let mut params: Vec = line[idx..c_idx].split(' ').map(|x| x.to_string()).collect(); - if c_idx != line.len() { - params.push(line[c_idx + 2..].to_string()); - } - - Ok(Line::new(tags, source, command, params)) + Ok(parsed_lines) } #[cfg(test)] @@ -187,38 +201,58 @@ mod test_lib { use std::collections::HashMap; #[test] - fn test_partial() { - let line = parse("PRIVMSG #rickastley :Never gonna give you up!").unwrap(); - assert_eq!(line.tags, HashMap::new()); - assert_eq!(line.source, None); - assert_eq!(line.command, "PRIVMSG"); - assert_eq!(line.params, vec!["#rickastley", "Never gonna give you up!"]); + fn test_single_partial() { + let msg = "PRIVMSG #rickastley :Never gonna give you up!"; + match parse(msg) { + Ok(mut x) => { + let line = x.pop_front().unwrap(); + + assert_eq!(line.tags, HashMap::new()); + assert_eq!(line.source, None); + assert_eq!(line.command, "PRIVMSG"); + assert_eq!(line.params, vec!["#rickastley", "Never gonna give you up!"]); + } + Err(e) => { + println!("A parsing error occured: {e}"); + return; + } + } } #[test] - fn test_full() { - let line = parse("@id=123;name=rick :nick!user@host.tmi.twitch.tv PRIVMSG #rickastley :Never gonna give you up!").unwrap(); - assert_eq!( - line.tags, - hashmap! { - String::from("id") => String::from("123"), - String::from("name") => String::from("rick"), + fn test_single_full() { + let msg = "@id=123;name=rick :nick!user@host.tmi.twitch.tv PRIVMSG #rickastley :Never gonna give you up!"; + match parse(msg) { + Ok(mut x) => { + let line = x.pop_front().unwrap(); + + assert_eq!( + line.tags, + hashmap! { + String::from("id") => String::from("123"), + String::from("name") => String::from("rick"), + } + ); + assert_eq!( + line.source, + Some(String::from(":nick!user@host.tmi.twitch.tv")) + ); + assert_eq!(line.command, "PRIVMSG"); + assert_eq!(line.params, vec!["#rickastley", "Never gonna give you up!"]); } - ); - assert_eq!( - line.source, - Some(String::from(":nick!user@host.tmi.twitch.tv")) - ); - assert_eq!(line.command, "PRIVMSG"); - assert_eq!(line.params, vec!["#rickastley", "Never gonna give you up!"]); + Err(e) => { + println!("A parsing error occured: {e}"); + return; + } + }; } #[test] fn test_readme_example() { let msg = "@id=123;name=rick :nick!user@host.tmi.twitch.tv PRIVMSG #rickastley :Never gonna give you up!"; match parse(msg) { - Ok(x) => { - let line = x; + Ok(mut x) => { + let line = x.pop_front().unwrap(); assert_eq!(&line.tags["id"], "123"); if line.source.is_some() { @@ -234,4 +268,27 @@ mod test_lib { } }; } + + #[test] + fn test_multiline() { + let msg = "@id=123 PRIVMSG #rickastley :Never gonna give you up!\n@id=456 PRIVMSG #rickastley :Never gonna let you down!"; + match parse(msg) { + Ok(mut x) => { + assert_eq!(x.len(), 2); + + let l1 = x.pop_front().unwrap(); + let l2 = x.pop_front().unwrap(); + + assert_eq!(&l1.tags["id"], "123"); + assert_eq!(&l2.tags["id"], "456"); + assert_eq!(l1.command, l2.command); + assert_eq!(l1.params[1], "Never gonna give you up!"); + assert_eq!(l2.params[1], "Never gonna let you down!"); + } + Err(e) => { + println!("A parsing error occured: {e}"); + return; + } + } + } }