This commit is contained in:
Daniel Flanagan 2024-07-06 18:18:34 -05:00
parent 9bf90e32b1
commit f487d2f23d
3 changed files with 243 additions and 208 deletions

View file

@ -1,208 +1,2 @@
mod song { mod display;
use std::{ mod song;
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<String>;
pub struct Song {
pub name: String,
pub verses: BTreeMap<String, Verse>,
pub other_plans: BTreeMap<String, Plan>,
pub default_plan: Plan,
}
impl Song {
pub fn plan(&self, plan_name: Option<String>) -> &VecDeque<String> {
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<Self, Self::Err> {
if s == "" {
return Err(SongParseError::EmptyString);
}
static HUNK_REGEX: OnceLock<Regex> = 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<String> = 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()
]))
);
}
}
}
mod display {
use super::song::{Plan, Song};
pub struct PlaylistEntry {
pub song: Song,
pub map: Plan,
}
pub struct PlaylistVerseRef {
pub song_index: usize,
pub song_map: String,
pub map_verse_index: usize,
}
pub struct Display {
pub playlist: Vec<(Song, Option<String>)>,
pub current: PlaylistVerseRef,
pub frozen_at: Option<PlaylistVerseRef>,
pub blanked: bool,
}
}

19
src/model/display.rs Normal file
View file

@ -0,0 +1,19 @@
use super::song::{Plan, Song};
pub struct PlaylistEntry {
pub song: Song,
pub map: Plan,
}
pub struct PlaylistVerseRef {
pub song_index: usize,
pub song_map: String,
pub map_verse_index: usize,
}
pub struct Display {
pub playlist: Vec<(Song, Option<String>)>,
pub current: PlaylistVerseRef,
pub frozen_at: Option<PlaylistVerseRef>,
pub blanked: bool,
}

222
src/model/song.rs Normal file
View file

@ -0,0 +1,222 @@
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<String>;
pub struct Song {
pub name: String,
pub verses: BTreeMap<String, Verse>,
pub other_plans: BTreeMap<String, Plan>,
pub default_plan: Plan,
}
impl Song {
pub fn plan(&self, plan_name: Option<String>) -> &VecDeque<String> {
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<Self, Self::Err> {
if s == "" {
return Err(SongParseError::EmptyString);
}
static HUNK_REGEX: OnceLock<Regex> = 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<String> = 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()
]))
);
}
}