use core::fmt;
use std::collections::BTreeMap;
use std::str::FromStr;
extern crate serde;
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum SongError {
DuplicatePartId(SongPartId),
MalformedPartId(String),
}
impl fmt::Display for SongError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SongError::DuplicatePartId(id) => {
write!(f, "the song already contains a part with the id '{}'", id)
}
SongError::MalformedPartId(text) => write!(
f,
"'{}' is not a valid song part id (expected '<type>.<number>', e.g. 'verse.1')",
text
),
}
}
}
impl std::error::Error for SongError {}
#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
pub enum SongPartType {
Verse,
Chorus,
Bridge,
Intro,
Outro,
Interlude,
Instrumental,
Solo,
PreChorus,
PostChorus,
Refrain,
Other,
}
impl SongPartType {
pub fn as_str(&self) -> &'static str {
match self {
SongPartType::Verse => "verse",
SongPartType::Chorus => "chorus",
SongPartType::Bridge => "bridge",
SongPartType::Intro => "intro",
SongPartType::Outro => "outro",
SongPartType::Interlude => "interlude",
SongPartType::Instrumental => "instrumental",
SongPartType::Solo => "solo",
SongPartType::PreChorus => "prechorus",
SongPartType::PostChorus => "postchorus",
SongPartType::Refrain => "refrain",
SongPartType::Other => "other",
}
}
pub fn is_chorus_like(&self) -> bool {
matches!(self, SongPartType::Chorus | SongPartType::Refrain)
}
pub fn is_repeatable(&self) -> bool {
match self {
SongPartType::Chorus
| SongPartType::PreChorus
| SongPartType::PostChorus
| SongPartType::Refrain => true,
SongPartType::Verse
| SongPartType::Bridge
| SongPartType::Intro
| SongPartType::Outro
| SongPartType::Interlude
| SongPartType::Instrumental
| SongPartType::Solo
| SongPartType::Other => false,
}
}
}
impl fmt::Display for SongPartType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for SongPartType {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.trim().to_lowercase().as_str() {
"verse" | "stanza" => SongPartType::Verse,
"chorus" => SongPartType::Chorus,
"bridge" => SongPartType::Bridge,
"intro" => SongPartType::Intro,
"outro" => SongPartType::Outro,
"interlude" => SongPartType::Interlude,
"instrumental" => SongPartType::Instrumental,
"solo" => SongPartType::Solo,
"prechorus" | "pre-chorus" => SongPartType::PreChorus,
"postchorus" | "post-chorus" => SongPartType::PostChorus,
"refrain" => SongPartType::Refrain,
_ => SongPartType::Other,
})
}
}
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
pub struct SongPartId {
pub part_type: SongPartType,
pub number: u32,
}
impl SongPartId {
pub fn new(part_type: SongPartType, number: u32) -> SongPartId {
SongPartId { part_type, number }
}
pub fn parse(id: &str) -> Option<SongPartId> {
id.parse().ok()
}
}
impl fmt::Display for SongPartId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.part_type, self.number)
}
}
impl FromStr for SongPartId {
type Err = SongError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let text = s.trim();
let (type_text, number_text) = text
.rsplit_once('.')
.ok_or_else(|| SongError::MalformedPartId(s.to_string()))?;
if type_text.is_empty() || !type_text.chars().all(|c| c.is_ascii_alphabetic() || c == '-') {
return Err(SongError::MalformedPartId(s.to_string()));
}
let number: u32 = number_text
.parse()
.map_err(|_| SongError::MalformedPartId(s.to_string()))?;
let part_type = type_text.parse().unwrap();
Ok(SongPartId { part_type, number })
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Debug)]
pub enum LyricLanguage {
Default,
Specific(String),
}
impl LyricLanguage {
pub fn specific(code: &str) -> LyricLanguage {
LyricLanguage::Specific(code.trim().to_lowercase())
}
pub fn code(&self) -> Option<&str> {
match self {
LyricLanguage::Default => None,
LyricLanguage::Specific(code) => Some(code),
}
}
pub fn matches(&self, wanted: &str, song_default: Option<&str>) -> bool {
let wanted = wanted.trim().to_lowercase();
match self {
LyricLanguage::Specific(code) => *code == wanted,
LyricLanguage::Default => song_default
.map(|default| default.trim().to_lowercase() == wanted)
.unwrap_or(false),
}
}
}
impl fmt::Display for LyricLanguage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LyricLanguage::Default => f.write_str("default"),
LyricLanguage::Specific(code) => f.write_str(code),
}
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Debug)]
pub enum SongPartContentType {
LeadVoice,
SupranoVoice,
AltoVoice,
TenorVoice,
BassVoice,
Instrumental,
Solo,
Chords,
Lyrics { language: LyricLanguage },
}
impl SongPartContentType {
pub fn is_lyrics(&self) -> bool {
matches!(self, SongPartContentType::Lyrics { .. })
}
pub fn is_voice(&self) -> bool {
matches!(
self,
SongPartContentType::LeadVoice
| SongPartContentType::SupranoVoice
| SongPartContentType::AltoVoice
| SongPartContentType::TenorVoice
| SongPartContentType::BassVoice
)
}
}
impl fmt::Display for SongPartContentType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SongPartContentType::LeadVoice => f.write_str("LeadVoice"),
SongPartContentType::SupranoVoice => f.write_str("SupranoVoice"),
SongPartContentType::AltoVoice => f.write_str("AltoVoice"),
SongPartContentType::TenorVoice => f.write_str("TenorVoice"),
SongPartContentType::BassVoice => f.write_str("BassVoice"),
SongPartContentType::Instrumental => f.write_str("Instrumental"),
SongPartContentType::Solo => f.write_str("Solo"),
SongPartContentType::Chords => f.write_str("Chords"),
SongPartContentType::Lyrics { language } => match language {
LyricLanguage::Default => f.write_str("Lyrics"),
LyricLanguage::Specific(code) => write!(f, "Lyrics ({})", code),
},
}
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
pub struct SongPartContent {
pub content_type: SongPartContentType,
pub content: String,
}
impl SongPartContent {
pub fn new(content_type: SongPartContentType, content: impl Into<String>) -> SongPartContent {
SongPartContent {
content_type,
content: content.into(),
}
}
pub fn lyrics(language: LyricLanguage, content: impl Into<String>) -> SongPartContent {
SongPartContent::new(SongPartContentType::Lyrics { language }, content)
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub struct SongPart {
pub part_type: SongPartType,
pub number: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
pub contents: Vec<SongPartContent>,
pub is_repetition_of: Option<SongPartId>,
}
impl SongPart {
pub fn new(id: SongPartId) -> SongPart {
SongPart {
part_type: id.part_type,
number: id.number,
label: None,
contents: Vec::new(),
is_repetition_of: None,
}
}
pub fn id(&self) -> SongPartId {
SongPartId::new(self.part_type, self.number)
}
pub fn display_label(&self) -> String {
match &self.label {
Some(label) => label.clone(),
None => self.id().to_string(),
}
}
pub fn add_content(&mut self, content: SongPartContent) {
self.contents.push(content);
}
pub fn content(&self, content_type: &SongPartContentType) -> Option<&SongPartContent> {
self.contents
.iter()
.find(|content| content.content_type == *content_type)
}
pub fn own_voice(&self) -> Option<&SongPartContent> {
self.contents
.iter()
.find(|content| content.content_type.is_voice())
}
pub fn has_lyrics(&self) -> bool {
self.contents
.iter()
.any(|content| content.content_type.is_lyrics())
}
pub fn all_lyrics(&self) -> impl Iterator<Item = (&LyricLanguage, &SongPartContent)> {
self.contents.iter().filter_map(|content| {
match &content.content_type {
SongPartContentType::Lyrics { language } => Some((language, content)),
_ => None,
}
})
}
pub fn lyrics_in(&self, language: &LyricLanguage) -> Option<&SongPartContent> {
self.all_lyrics()
.find(|(candidate, _)| *candidate == language)
.map(|(_, content)| content)
}
pub fn lyrics_for(
&self,
wanted: Option<&str>,
song_default: Option<&str>,
) -> Option<&SongPartContent> {
if let Some(wanted) = wanted
&& let Some((_, content)) = self
.all_lyrics()
.find(|(language, _)| language.matches(wanted, song_default))
{
return Some(content);
}
if let Some(default) = song_default
&& let Some((_, content)) = self
.all_lyrics()
.find(|(language, _)| language.matches(default, song_default))
{
return Some(content);
}
self.all_lyrics()
.find(|(language, _)| **language == LyricLanguage::Default)
.or_else(|| self.all_lyrics().next())
.map(|(_, content)| content)
}
pub fn is_repeatable(&self) -> bool {
self.part_type.is_repeatable()
}
}
#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)]
pub struct ScoreSettings {
pub key: Option<String>,
pub time: Option<String>,
pub partial: Option<u32>,
}
impl ScoreSettings {
pub fn is_empty(&self) -> bool {
self.key.is_none() && self.time.is_none() && self.partial.is_none()
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug, Default)]
pub struct Song {
pub title: String,
pub default_language: Option<String>,
pub score: ScoreSettings,
tags: BTreeMap<String, String>,
parts: Vec<SongPart>,
pub part_orders: Vec<PartOrder>,
}
impl Song {
pub fn new(title: &str) -> Song {
Song {
title: title.to_string(),
..Song::default()
}
}
pub fn set_tag(&mut self, key: &str, value: &str) {
self.tags.insert(key.to_string(), value.to_string());
}
pub fn tag(&self, key: &str) -> Option<&String> {
self.tags.get(key)
}
pub fn remove_tag(&mut self, key: &str) -> Option<String> {
self.tags.remove(key)
}
pub fn tags(&self) -> &BTreeMap<String, String> {
&self.tags
}
pub fn parts(&self) -> &[SongPart] {
&self.parts
}
pub fn parts_mut(&mut self) -> &mut [SongPart] {
&mut self.parts
}
pub fn part_count(&self) -> usize {
self.parts.len()
}
pub fn part(&self, id: &SongPartId) -> Option<&SongPart> {
self.parts.iter().find(|part| part.id() == *id)
}
pub fn part_mut(&mut self, id: &SongPartId) -> Option<&mut SongPart> {
self.parts.iter_mut().find(|part| part.id() == *id)
}
pub fn part_at(&self, index: usize) -> Option<&SongPart> {
self.parts.get(index)
}
pub fn parts_of_type(&self, part_type: SongPartType) -> impl Iterator<Item = &SongPart> {
self.parts
.iter()
.filter(move |part| part.part_type == part_type)
}
pub fn part_count_of_type(&self, part_type: SongPartType) -> u32 {
self.parts_of_type(part_type).count() as u32
}
pub fn chorus_like_parts(&self) -> impl Iterator<Item = &SongPart> {
self.parts
.iter()
.filter(|part| part.part_type.is_chorus_like())
}
pub fn add_part(&mut self, part: SongPart) -> Result<SongPartId, SongError> {
let id = part.id();
if self.part(&id).is_some() {
return Err(SongError::DuplicatePartId(id));
}
self.parts.push(part);
Ok(id)
}
pub fn add_part_of_type(
&mut self,
part_type: SongPartType,
number: Option<u32>,
) -> SongPartId {
let mut candidate = number.unwrap_or_else(|| self.part_count_of_type(part_type) + 1);
while self.part(&SongPartId::new(part_type, candidate)).is_some() {
candidate += 1;
}
let id = SongPartId::new(part_type, candidate);
self.parts.push(SongPart::new(id));
id
}
pub fn parts_with_content(&self, content: &str) -> Vec<&SongPart> {
let needle = content.trim().to_lowercase();
self.parts
.iter()
.filter(|part| {
part.contents
.iter()
.any(|item| item.content.trim().to_lowercase() == needle)
})
.collect()
}
pub fn last_part_with_content(&self, content: &str) -> Option<&SongPart> {
self.parts_with_content(content).last().copied()
}
pub fn content_types(&self) -> Vec<SongPartContentType> {
let mut types: Vec<SongPartContentType> = Vec::new();
for part in &self.parts {
for content in &part.contents {
if !types.contains(&content.content_type) {
types.push(content.content_type.clone());
}
}
}
types
}
pub fn voice_for_part<'a>(&'a self, part: &'a SongPart) -> Option<&'a SongPartContent> {
let mut current = part;
let mut visited: Vec<SongPartId> = vec![current.id()];
loop {
if let Some(voice) = current.own_voice() {
return Some(voice);
}
let next_id = current.is_repetition_of?;
if visited.contains(&next_id) {
return None;
}
visited.push(next_id);
current = self.part(&next_id)?;
}
}
pub fn available_languages(&self) -> Vec<String> {
let mut languages: Vec<String> = self
.parts
.iter()
.flat_map(|part| part.all_lyrics())
.filter_map(|(language, _)| language.code().map(|code| code.to_string()))
.collect();
languages.sort();
languages.dedup();
languages
}
pub fn add_guessed_part_order(&mut self) {
let order = PartOrder::from_guess(self);
self.part_orders.push(order);
}
pub fn ordered_parts(&self) -> Vec<&SongPart> {
match self.part_orders.first() {
Some(order) => order.to_parts(self),
None => self.parts.iter().collect(),
}
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
pub enum PartOrderName {
Default,
Custom(String),
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
pub struct PartOrder {
pub name: PartOrderName,
rule: PartOrderRule,
}
impl PartOrder {
pub fn new(name: PartOrderName, rule: PartOrderRule) -> PartOrder {
PartOrder { name, rule }
}
pub fn rule(&self) -> &PartOrderRule {
&self.rule
}
pub fn from_guess(song: &Song) -> PartOrder {
if song.part_count() < 2 {
return PartOrder::new(
PartOrderName::Default,
PartOrderRule::Custom(song.parts().iter().map(|part| part.id()).collect()),
);
}
let first_type = song.part_at(0).unwrap().part_type;
if first_type == SongPartType::Verse {
return PartOrder::new(
PartOrderName::Default,
PartOrderRule::VerseRefrainBridgeRefrain,
);
}
if first_type.is_chorus_like() {
return PartOrder::new(
PartOrderName::Default,
PartOrderRule::RefrainVerseBridgeRefrain,
);
}
if song.chorus_like_parts().next().is_none()
|| song.part_count_of_type(SongPartType::Bridge) == 0
{
return PartOrder::new(
PartOrderName::Default,
PartOrderRule::VerseRefrainBridgeRefrain,
);
}
PartOrder::new(
PartOrderName::Default,
PartOrderRule::Custom(song.parts().iter().map(|part| part.id()).collect()),
)
}
pub fn is_refrain_first(&self) -> bool {
matches!(self.rule, PartOrderRule::RefrainVerseBridgeRefrain)
}
pub fn to_part_ids(&self, song: &Song) -> Vec<SongPartId> {
match &self.rule {
PartOrderRule::Custom(ids) => ids
.iter()
.filter(|id| song.part(id).is_some())
.copied()
.collect(),
PartOrderRule::VerseRefrainBridgeRefrain => interleave_verses_and_refrain(song, false),
PartOrderRule::RefrainVerseBridgeRefrain => interleave_verses_and_refrain(song, true),
}
}
pub fn to_parts<'song>(&self, song: &'song Song) -> Vec<&'song SongPart> {
self.to_part_ids(song)
.into_iter()
.filter_map(|id| song.part(&id))
.collect()
}
}
fn interleave_verses_and_refrain(song: &Song, refrain_first: bool) -> Vec<SongPartId> {
let verses: Vec<SongPartId> = song
.parts_of_type(SongPartType::Verse)
.map(|part| part.id())
.collect();
let refrain = song.chorus_like_parts().next().map(|part| part.id());
let prechorus = song
.parts_of_type(SongPartType::PreChorus)
.next()
.map(|part| part.id());
let bridge = song
.parts_of_type(SongPartType::Bridge)
.next()
.map(|part| part.id());
if verses.is_empty() {
return song.parts().iter().map(|part| part.id()).collect();
}
if refrain.is_none() && bridge.is_none() && prechorus.is_none() {
return verses;
}
let mut order: Vec<SongPartId> = Vec::new();
if refrain_first
&& let Some(refrain) = refrain {
order.push(refrain);
}
let last_verse = verses.len() - 1;
for (index, verse) in verses.iter().enumerate() {
order.push(*verse);
if index == last_verse
&& let Some(bridge) = bridge {
order.push(bridge);
}
if let Some(prechorus) = prechorus {
order.push(prechorus);
}
if let Some(refrain) = refrain {
order.push(refrain);
}
}
order
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
pub enum PartOrderRule {
VerseRefrainBridgeRefrain,
RefrainVerseBridgeRefrain,
Custom(Vec<SongPartId>),
}
#[cfg(test)]
mod tests {
use super::*;
fn lyrics_part(song: &mut Song, part_type: SongPartType, text: &str) -> SongPartId {
let id = song.add_part_of_type(part_type, None);
song.part_mut(&id)
.unwrap()
.add_content(SongPartContent::lyrics(LyricLanguage::Default, text));
id
}
#[test]
fn test_part_id_roundtrip_is_case_insensitive() {
let canonical: SongPartId = "verse.1".parse().unwrap();
assert_eq!("Verse.1".parse::<SongPartId>().unwrap(), canonical);
assert_eq!("STANZA.1".parse::<SongPartId>().unwrap(), canonical);
assert_eq!(canonical.to_string(), "verse.1");
}
#[test]
fn test_part_id_rejects_surrounding_junk() {
assert!("please sing verse.1 now".parse::<SongPartId>().is_err());
assert!("verse".parse::<SongPartId>().is_err());
assert!("verse.".parse::<SongPartId>().is_err());
assert!("verse.x".parse::<SongPartId>().is_err());
assert!(SongPartId::parse("abcdefg").is_none());
}
#[test]
fn test_part_id_cannot_drift_from_its_part() {
let mut part = SongPart::new(SongPartId::new(SongPartType::Verse, 1));
assert_eq!(part.id().to_string(), "verse.1");
part.part_type = SongPartType::Refrain;
assert_eq!(part.id().to_string(), "refrain.1");
}
#[test]
fn test_lookup_by_id() {
let mut song = Song::new("Test");
song.add_part_of_type(SongPartType::Verse, Some(1));
assert!(song.part(&"verse.1".parse().unwrap()).is_some());
assert!(song.part(&"Verse.1".parse().unwrap()).is_some());
assert!(song.part(&"stanza.1".parse().unwrap()).is_some());
assert!(song.part(&"verse.2".parse().unwrap()).is_none());
}
#[test]
fn test_add_part_rejects_duplicates() {
let mut song = Song::new("Test");
let id = SongPartId::new(SongPartType::Verse, 1);
assert!(song.add_part(SongPart::new(id)).is_ok());
let error = song.add_part(SongPart::new(id)).unwrap_err();
assert_eq!(error, SongError::DuplicatePartId(id));
assert_eq!(song.part_count(), 1);
}
#[test]
fn test_add_part_of_type_never_duplicates() {
let mut song = Song::new("Test");
assert_eq!(song.add_part_of_type(SongPartType::Verse, None).number, 1);
assert_eq!(song.add_part_of_type(SongPartType::Verse, Some(1)).number, 2);
assert_eq!(song.add_part_of_type(SongPartType::Verse, Some(5)).number, 5);
assert_eq!(song.part_count(), 3);
}
#[test]
fn test_part_counts() {
let mut song = Song::new("Test");
lyrics_part(&mut song, SongPartType::Verse, "one");
lyrics_part(&mut song, SongPartType::Verse, "two");
lyrics_part(&mut song, SongPartType::Refrain, "ref");
assert_eq!(song.part_count_of_type(SongPartType::Verse), 2);
assert_eq!(song.part_count_of_type(SongPartType::Refrain), 1);
assert_eq!(song.chorus_like_parts().count(), 1);
assert_eq!(song.part_count(), 3);
}
#[test]
fn test_voice_is_resolved_through_repetitions() {
let mut song = Song::new("Test");
let first = song.add_part_of_type(SongPartType::Verse, None);
song.part_mut(&first).unwrap().add_content(SongPartContent::new(
SongPartContentType::LeadVoice,
"c4 d e f",
));
let second = song.add_part_of_type(SongPartType::Verse, None);
song.part_mut(&second).unwrap().is_repetition_of = Some(first);
let part = song.part(&second).unwrap();
assert_eq!(song.voice_for_part(part).unwrap().content, "c4 d e f");
}
#[test]
fn test_repetition_cycle_does_not_hang() {
let mut song = Song::new("Test");
let a = song.add_part_of_type(SongPartType::Verse, None);
let b = song.add_part_of_type(SongPartType::Verse, None);
song.part_mut(&a).unwrap().is_repetition_of = Some(b);
song.part_mut(&b).unwrap().is_repetition_of = Some(a);
assert!(song.voice_for_part(song.part(&a).unwrap()).is_none());
}
#[test]
fn test_repetition_of_missing_part_is_not_fatal() {
let mut song = Song::new("Test");
let id = song.add_part_of_type(SongPartType::Verse, None);
song.part_mut(&id).unwrap().is_repetition_of =
Some(SongPartId::new(SongPartType::Verse, 99));
assert!(song.voice_for_part(song.part(&id).unwrap()).is_none());
}
#[test]
fn test_language_codes_are_normalised() {
assert_eq!(LyricLanguage::specific("EN"), LyricLanguage::specific("en"));
assert_eq!(LyricLanguage::specific(" de ").code(), Some("de"));
}
#[test]
fn test_available_languages_is_sorted_and_deduplicated() {
let mut song = Song::new("Test");
let id = song.add_part_of_type(SongPartType::Verse, None);
{
let part = song.part_mut(&id).unwrap();
part.add_content(SongPartContent::lyrics(LyricLanguage::specific("de"), "a"));
part.add_content(SongPartContent::lyrics(LyricLanguage::specific("en"), "b"));
part.add_content(SongPartContent::lyrics(LyricLanguage::Default, "c"));
}
let other = song.add_part_of_type(SongPartType::Verse, None);
song.part_mut(&other)
.unwrap()
.add_content(SongPartContent::lyrics(LyricLanguage::specific("de"), "d"));
assert_eq!(song.available_languages(), vec!["de", "en"]);
}
#[test]
fn test_lyrics_fallback_chain() {
let mut part = SongPart::new(SongPartId::new(SongPartType::Verse, 1));
part.add_content(SongPartContent::lyrics(LyricLanguage::specific("en"), "English"));
part.add_content(SongPartContent::lyrics(LyricLanguage::Default, "Unlabelled"));
assert_eq!(part.lyrics_for(Some("en"), None).unwrap().content, "English");
assert_eq!(part.lyrics_for(Some("de"), Some("de")).unwrap().content, "Unlabelled");
assert_eq!(part.lyrics_for(Some("fr"), None).unwrap().content, "Unlabelled");
assert!(part.lyrics_for(None, None).is_some());
}
#[test]
fn test_refrain_is_repeated_after_every_verse() {
let mut song = Song::new("Test");
lyrics_part(&mut song, SongPartType::Verse, "one");
lyrics_part(&mut song, SongPartType::Verse, "two");
lyrics_part(&mut song, SongPartType::Refrain, "ref");
song.add_guessed_part_order();
let ids: Vec<String> = song
.ordered_parts()
.iter()
.map(|part| part.id().to_string())
.collect();
assert_eq!(ids, ["verse.1", "refrain.1", "verse.2", "refrain.1"]);
}
#[test]
fn test_refrain_first_order() {
let mut song = Song::new("Test");
lyrics_part(&mut song, SongPartType::Refrain, "ref");
lyrics_part(&mut song, SongPartType::Verse, "one");
lyrics_part(&mut song, SongPartType::Verse, "two");
song.add_guessed_part_order();
assert!(song.part_orders[0].is_refrain_first());
let ids: Vec<String> = song
.ordered_parts()
.iter()
.map(|part| part.id().to_string())
.collect();
assert_eq!(
ids,
["refrain.1", "verse.1", "refrain.1", "verse.2", "refrain.1"]
);
}
#[test]
fn test_bridge_comes_before_the_last_refrain() {
let mut song = Song::new("Test");
lyrics_part(&mut song, SongPartType::Verse, "one");
lyrics_part(&mut song, SongPartType::Verse, "two");
lyrics_part(&mut song, SongPartType::Refrain, "ref");
lyrics_part(&mut song, SongPartType::Bridge, "bridge");
song.add_guessed_part_order();
let ids: Vec<String> = song
.ordered_parts()
.iter()
.map(|part| part.id().to_string())
.collect();
assert_eq!(
ids,
["verse.1", "refrain.1", "verse.2", "bridge.1", "refrain.1"]
);
}
#[test]
fn test_custom_order_skips_unknown_ids() {
let mut song = Song::new("Test");
lyrics_part(&mut song, SongPartType::Verse, "one");
song.part_orders.push(PartOrder::new(
PartOrderName::Custom("short".to_string()),
PartOrderRule::Custom(vec![
SongPartId::new(SongPartType::Verse, 1),
SongPartId::new(SongPartType::Refrain, 9),
]),
));
let parts = song.part_orders[0].to_parts(&song);
assert_eq!(parts.len(), 1);
assert_eq!(parts[0].id().to_string(), "verse.1");
}
#[test]
fn test_custom_order_survives_a_serde_round_trip() {
let mut song = Song::new("Round trip");
lyrics_part(&mut song, SongPartType::Verse, "one");
lyrics_part(&mut song, SongPartType::Refrain, "ref");
song.part_orders.push(PartOrder::new(
PartOrderName::Custom("short".to_string()),
PartOrderRule::Custom(vec![
SongPartId::new(SongPartType::Refrain, 1),
SongPartId::new(SongPartType::Verse, 1),
]),
));
let json = serde_json::to_string(&song).unwrap();
let restored: Song = serde_json::from_str(&json).unwrap();
assert_eq!(restored, song);
let ordered = restored.part_orders[0].to_parts(&restored);
assert_eq!(ordered.len(), 2);
for part in ordered {
assert!(std::ptr::eq(part, restored.part(&part.id()).unwrap()));
}
assert_eq!(json.matches("\"Refrain\"").count(), 2, "{}", json);
}
#[test]
fn test_serialisation_is_deterministic() {
let mut song = Song::new("Tags");
song.set_tag("author", "A");
song.set_tag("copyright", "C");
song.set_tag("bible", "B");
let first = serde_json::to_string(&song).unwrap();
let second = serde_json::to_string(&song).unwrap();
assert_eq!(first, second);
assert!(first.find("author").unwrap() < first.find("bible").unwrap());
}
#[test]
fn test_song_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Song>();
assert_send_sync::<SongPart>();
assert_send_sync::<PartOrder>();
}
#[test]
fn test_tags() {
let mut song = Song::new("Test");
song.set_tag("author", "first");
song.set_tag("author", "second");
assert_eq!(song.tag("author").unwrap(), "second");
assert_eq!(song.tags().len(), 1);
assert_eq!(song.remove_tag("author").as_deref(), Some("second"));
assert!(song.tag("author").is_none());
}
#[test]
fn test_content_types_are_distinct() {
let mut song = Song::new("Test");
let verse = song.add_part_of_type(SongPartType::Verse, None);
song.part_mut(&verse).unwrap().add_content(SongPartContent::lyrics(
LyricLanguage::Default,
"text",
));
let refrain = song.add_part_of_type(SongPartType::Refrain, None);
song.part_mut(&refrain)
.unwrap()
.add_content(SongPartContent::new(
SongPartContentType::LeadVoice,
"c4 d4",
));
assert_eq!(song.content_types().len(), 2);
}
}