use std::iter; use std::str::FromStr; use evtclib::{Log, Outcome}; use itertools::Itertools; /// A [`LogBag`] is a struct that holds multiple logs in their categories. /// /// This is similar to hash map mapping a category to a list of logs, but the [`LogBag`] saves its /// insertion order. The logs are saved as a line, that way we don't have to re-parse or re-upload /// them and we can just handle arbitrary data. #[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] pub struct LogBag { data: Vec<(String, Vec)>, } // Conditional compilation makes it hard to really use all the code, so we just allow dead code // here locally. #[allow(dead_code)] impl LogBag { /// Construct a new, empty [`LogBag`]. pub fn new() -> Self { LogBag { data: Vec::new() } } /// Return an iterator over all available categories. pub fn categories(&self) -> impl Iterator { self.data.iter().map(|x| &x.0 as &str) } /// Return an iterator over (category, items). pub fn iter(&self) -> impl Iterator)> { self.data .iter() .map(|(cat, lines)| (cat as &str, lines.iter().map(|l| l as &str))) } /// Insert an item into the given category. /// /// If the category does not exist yet, it will be appended at the bottom. pub fn insert(&mut self, category: &str, line: String) { for (cat, lines) in self.data.iter_mut() { if cat == category { lines.push(line); return; } } // When we reach here, we don't have the category yet, so we gotta insert it. self.data.push((String::from(category), vec![line])); } /// Tries to parse the given text as a plain [`LogBag`]. pub fn parse_plain(input: &str) -> Option { input.parse().ok() } pub fn parse_markdown(input: &str) -> Option { let plain = input .split('\n') .map(|line| line.trim_matches('*')) .join("\n"); LogBag::parse_plain(&plain) } /// Renders the contents of this [`LogBag`] as plain text. /// /// The output of this can be fed back into [`LogBag::parse_plain`] to round-trip. pub fn render_plain(&self) -> String { self.iter() .map(|(category, lines)| iter::once(category).chain(lines).join("\n")) .join("\n\n") } /// Renders the contents of this [`LogBag`] as HTML. /// /// Useful for posting to Matrix chats. pub fn render_html(&self) -> String { self.iter() .map(|(category, mut lines)| { format!("{}
\n{}", category, lines.join("
\n")) }) .join("
\n
\n") } /// Renders the contents of this [`LogBag`] as Markdown. /// /// Useful for posting to Discord chats. pub fn render_markdown(&self) -> String { self.iter() .map(|(category, mut lines)| format!("**{}**\n{}", category, lines.join("\n"))) .join("\n\n") } } impl FromStr for LogBag { type Err = (); fn from_str(s: &str) -> Result { let data = s .trim() .split("\n\n") .map(|chunk| { let mut lines = chunk.split('\n'); let category = lines.next().unwrap(); ( category.to_string(), lines.map(ToString::to_string).collect::>(), ) }) .filter(|(cat, lines)| !cat.is_empty() && !lines.is_empty()) .collect(); Ok(LogBag { data }) } } impl From)>> for LogBag { fn from(data: Vec<(String, Vec)>) -> Self { LogBag { data } } } /// A helper function to return the right emoji for a given log. pub fn state_emoji(log: &Log) -> &'static str { let outcome = log.analyzer().and_then(|a| a.outcome()); match outcome { Some(Outcome::Success) => "✅", Some(Outcome::Failure) => "❌", None => "❓", } } #[cfg(test)] mod test { use super::*; #[test] fn insert() { let mut logbag = LogBag::new(); logbag.insert("cat 1", "line 1".to_string()); assert_eq!(logbag.categories().count(), 1); logbag.insert("cat 1", "line 2".to_string()); assert_eq!(logbag.categories().count(), 1); logbag.insert("cat 2", "line 1".to_string()); assert_eq!(logbag.categories().count(), 2); assert_eq!( logbag.categories().collect::>(), vec!["cat 1", "cat 2"] ); } #[test] fn parse_empty() { assert_eq!(LogBag::parse_plain(""), Some(LogBag::new())); } #[test] fn parse_single() { let mut logbag = LogBag::new(); logbag.insert("cat 1", "line 1".to_string()); logbag.insert("cat 1", "line 2".to_string()); assert_eq!( LogBag::parse_plain( "\ cat 1 line 1 line 2" ), Some(logbag) ); } #[test] fn parse_multi() { let mut logbag = LogBag::new(); logbag.insert("cat 1", "line 1".to_string()); logbag.insert("cat 1", "line 2".to_string()); logbag.insert("cat 2", "line 1".to_string()); logbag.insert("cat 2", "line 2".to_string()); assert_eq!( LogBag::parse_plain( "\ cat 1 line 1 line 2 cat 2 line 1 line 2" ), Some(logbag) ); } #[test] fn parse_markdown() { let mut logbag = LogBag::new(); logbag.insert("cat 1", "line 1".to_string()); logbag.insert("cat 1", "line 2".to_string()); logbag.insert("cat 2", "line 1".to_string()); logbag.insert("cat 2", "line 2".to_string()); assert_eq!( LogBag::parse_markdown( "\ **cat 1** line 1 line 2 **cat 2** line 1 line 2" ), Some(logbag) ); } #[test] fn render_plain_single() { let mut logbag = LogBag::new(); logbag.insert("category", "line 1".to_string()); logbag.insert("category", "line 2".to_string()); assert_eq!( logbag.render_plain(), "\ category line 1 line 2" ); } #[test] fn render_plain_multi() { let mut logbag = LogBag::new(); logbag.insert("category 1", "line 1".to_string()); logbag.insert("category 1", "line 2".to_string()); logbag.insert("category 2", "enil 1".to_string()); logbag.insert("category 2", "enil 2".to_string()); assert_eq!( logbag.render_plain(), "\ category 1 line 1 line 2 category 2 enil 1 enil 2" ); } #[test] fn render_html_single() { let mut logbag = LogBag::new(); logbag.insert("category", "line 1".to_string()); logbag.insert("category", "line 2".to_string()); assert_eq!( logbag.render_html(), "\ category
line 1
line 2" ); } #[test] fn render_html_multi() { let mut logbag = LogBag::new(); logbag.insert("category 1", "line 1".to_string()); logbag.insert("category 1", "line 2".to_string()); logbag.insert("category 2", "enil 1".to_string()); logbag.insert("category 2", "enil 2".to_string()); assert_eq!( logbag.render_html(), "\ category 1
line 1
line 2

category 2
enil 1
enil 2" ); } #[test] fn render_markdown_single() { let mut logbag = LogBag::new(); logbag.insert("category", "line 1".to_string()); logbag.insert("category", "line 2".to_string()); assert_eq!( logbag.render_markdown(), "\ **category** line 1 line 2" ); } #[test] fn render_markdown_multi() { let mut logbag = LogBag::new(); logbag.insert("category 1", "line 1".to_string()); logbag.insert("category 1", "line 2".to_string()); logbag.insert("category 2", "enil 1".to_string()); logbag.insert("category 2", "enil 2".to_string()); assert_eq!( logbag.render_markdown(), "\ **category 1** line 1 line 2 **category 2** enil 1 enil 2" ); } }