use std::{ collections::{BTreeMap, VecDeque}, str::FromStr, sync::OnceLock, }; use regex::Regex; #[derive(Debug, PartialEq, Eq)] pub struct Verse { // pub background: String, // url pub content: String, } impl Verse { fn new(content: String) -> Self { Self { content } } } /// Sequence of verse names. pub type Plan = VecDeque; pub struct Song { pub name: String, pub verses: BTreeMap, pub other_plans: BTreeMap, pub default_plan: Plan, } impl Song { pub fn plan(&self, plan_name: Option) -> &VecDeque { plan_name .map(|plan_name| { self.other_plans .get(&plan_name) .unwrap_or(&self.default_plan) }) .unwrap_or(&self.default_plan) } } #[derive(Debug)] pub struct SourceRef { line_number: usize, } #[derive(Debug)] pub enum SongParseError { EmptyString, InvalidMetadata(SourceRef), } impl FromStr for Song { type Err = SongParseError; fn from_str(s: &str) -> Result { if s == "" { return Err(SongParseError::EmptyString); } static HUNK_REGEX: OnceLock = OnceLock::new(); let re = HUNK_REGEX.get_or_init(|| Regex::new(r"\s*[\n\r]\s*[\n\r]\s*").unwrap()); let mut hunks = VecDeque::new(); let mut last_end: usize = 0; for m in re.find_iter(s) { hunks.push_back(&s[last_end..m.start()]); last_end = m.end(); } hunks.push_back(&s[last_end..s.len()]); // process header let mut header_lines = hunks.pop_front().unwrap().lines().map(|s| s.trim()); let name = header_lines.next().unwrap().trim().to_owned(); let mut other_plans = BTreeMap::new(); for (line_number, line) in header_lines.enumerate() { if line.starts_with("plan(") { if let Some(end) = line.find(")") { match line[end..].find(":") { Some(i) => { let plan_name = &line[5..end]; let entries: VecDeque = line[(end + i + 1)..] .trim() .split(',') .map(|s| s.trim().to_owned()) .collect(); other_plans.insert(plan_name.to_owned(), entries); } None => { return Err(SongParseError::InvalidMetadata(SourceRef { line_number })); } } } } // map(band2): slide1, slide2 // band2: slide1, slide2 } let mut verses = BTreeMap::new(); let mut default_plan = Plan::new(); // process verses for hunk in hunks { let mut verse_contents = hunk; let end_i = hunk.find('\n').unwrap_or(hunk.len()); let verse_name: String = if let Some(i) = &hunk[0..end_i].find(':') { verse_contents = &hunk[end_i + 1..]; String::from(&hunk[0..*i]) } else { format!("Generated Verse {}", verses.len() + 1).to_owned() }; verses.insert(verse_name.clone(), Verse::new(verse_contents.to_owned())); default_plan.push_back(verse_name.clone()); } Ok(Self { name, verses, other_plans, default_plan, }) } } mod test { use std::collections::VecDeque; use super::{Song, Verse}; #[test] fn parses_simple_song() { let song: Song = r#"Song Title A verse"# .parse() .unwrap(); assert_eq!(song.name, "Song Title"); assert_eq!( song.verses.get("Generated Verse 1"), Some(&Verse { content: "A verse".to_owned() }) ); assert_eq!(song.verses.len(), 1); assert_eq!(song.default_plan[0], "Generated Verse 1"); assert_eq!(song.default_plan.len(), 1); } #[test] fn parses_song_with_plan() { let song: Song = r#"Song Title plan(another_plan): Generated Verse 1, Generated Verse 1, Generated Verse 1 A verse"# .parse() .unwrap(); assert_eq!(song.name, "Song Title"); assert_eq!( song.verses.get("Generated Verse 1"), Some(&Verse { content: "A verse".to_owned() }) ); assert_eq!(song.verses.len(), 1); assert_eq!(song.default_plan[0], "Generated Verse 1"); assert_eq!(song.default_plan.len(), 1); dbg!(&song.other_plans); assert_eq!( song.other_plans.get("another_plan"), Some(&VecDeque::from(vec![ "Generated Verse 1".to_owned(), "Generated Verse 1".to_owned(), "Generated Verse 1".to_owned() ])) ); } #[test] fn parses_song_with_verse_ref() { let song: Song = r#"Title v1: v1 v2: v2 (v2) (v1)"# .parse() .unwrap(); assert_eq!(song.name, "Song Title"); assert_eq!( song.verses.get("Generated Verse 1"), Some(&Verse { content: "A verse".to_owned() }) ); assert_eq!(song.verses.len(), 1); assert_eq!(song.default_plan[0], "Generated Verse 1"); assert_eq!(song.default_plan.len(), 1); dbg!(&song.other_plans); assert_eq!( song.other_plans.get("another_plan"), Some(&VecDeque::from(vec![ "Generated Verse 1".to_owned(), "Generated Verse 1".to_owned(), "Generated Verse 1".to_owned() ])) ); } }