mod line;
pub use line::Line;
use std::collections::{HashMap, VecDeque};
type ParseResult<T> = Result<T, ParseError>;
#[derive(Debug, Clone)]
pub struct ParseError {
pub details: String,
}
impl ParseError {
pub fn new(details: &str) -> Self {
Self {
details: details.into(),
}
}
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.details)
}
}
fn find_index(text: &str, char: char, start: usize) -> Option<usize> {
for (k, _) in text.match_indices(char) {
if k > start {
return Some(k);
}
}
None
}
pub fn parse(text: &str) -> ParseResult<VecDeque<Line>> {
let mut parsed_lines: VecDeque<Line> = VecDeque::new();
for line in text.split("\r\n") {
if line.is_empty() {
continue;
}
let mut idx = 0;
let mut tags: HashMap<String, String> = HashMap::new();
let mut source: Option<String> = None;
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;
}
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;
}
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(),
};
let mut params: Vec<String> = 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));
}
Ok(parsed_lines)
}
#[cfg(test)]
mod test_lib {
use super::parse;
use collection_macros::hashmap;
use std::collections::HashMap;
#[test]
fn test_single_partial() {
let msg = "PRIVMSG #rickastley :Never gonna give you up!\r\n";
match parse(msg) {
Ok(mut x) => {
assert_eq!(x.len(), 1);
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}");
assert!(false);
}
}
}
#[test]
fn test_single_full() {
let msg = "@id=123;name=rick :nick!user@host.tmi.twitch.tv PRIVMSG #rickastley :Never gonna give you up!\r\n";
match parse(msg) {
Ok(mut x) => {
assert_eq!(x.len(), 1);
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!"]);
}
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!\r\n";
match parse(msg) {
Ok(mut x) => {
println!("{x:?}");
assert_eq!(x.len(), 1);
let line = x.pop_front().unwrap();
assert_eq!(&line.tags["id"], "123");
if line.source.is_some() {
assert_eq!(line.source.unwrap(), ":nick!user@host.tmi.twitch.tv");
}
assert_eq!(line.command, "PRIVMSG");
assert_eq!(line.params[0], "#rickastley");
assert_eq!(line.params[1], "Never gonna give you up!");
}
Err(e) => {
println!("A parsing error occured: {e}");
assert!(false);
}
};
}
#[test]
fn test_empty() {
match parse("") {
Ok(x) => {
assert_eq!(x.len(), 0);
}
Err(e) => {
println!("A parsing error occured: {e}");
assert!(false);
}
};
}
#[test]
fn test_multiline() {
let msg = "@id=123 PRIVMSG #rickastley :Never gonna give you up!\r\n@id=456 PRIVMSG #rickastley :Never gonna let you down!\r\n";
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}");
assert!(false);
}
}
}
}