From d004698923c9047d67fc578a87f8d7cd917afb11 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 8 Apr 2026 01:34:26 +0200 Subject: [PATCH] feat(domain): add Song, Section, LyricLine, ChordPosition types --- crates/domain/src/lib.rs | 2 + crates/domain/src/song.rs | 88 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 crates/domain/src/song.rs diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 3b1635d..68b83fe 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -1,5 +1,7 @@ pub mod note; pub mod chord; +pub mod song; pub use note::Note; pub use chord::Chord; +pub use song::{ChordPosition, LyricLine, Section, SectionKind, SongMeta, Song}; diff --git a/crates/domain/src/song.rs b/crates/domain/src/song.rs new file mode 100644 index 0000000..e3f26f6 --- /dev/null +++ b/crates/domain/src/song.rs @@ -0,0 +1,88 @@ +use serde::{Deserialize, Serialize}; +use crate::Chord; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChordPosition { + pub offset: usize, + pub chord: Chord, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LyricLine { + pub text: String, + pub chords: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SectionKind { + Verse, Chorus, Bridge, PreChorus, + Intro, Outro, Break, Tab, + Other(String), +} + +impl SectionKind { + pub fn from_label(s: &str) -> Self { + match s.to_lowercase().replace(['-', '_', ' '], "").as_str() { + "verse" => Self::Verse, + "chorus" => Self::Chorus, + "bridge" => Self::Bridge, + "prechorus" => Self::PreChorus, + "intro" => Self::Intro, + "outro" => Self::Outro, + "break" => Self::Break, + "tab" => Self::Tab, + _ => Self::Other(s.to_string()), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Section { + pub kind: SectionKind, + pub label: Option, + pub lines: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SongMeta { + pub title: String, + pub artist: String, + pub capo: Option, + pub original_key: Option, + pub tuning: Option, + pub tempo: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Song { + pub meta: SongMeta, + pub sections: Vec
, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Chord, Note}; + + #[test] + fn lyric_line_chord_positions() { + let line = LyricLine { + text: "A drop in the ocean".into(), + chords: vec![ + ChordPosition { offset: 0, chord: Chord { root: Note::E, descriptor: Some("m".into()) } }, + ChordPosition { offset: 8, chord: Chord { root: Note::C, descriptor: None } }, + ], + }; + assert_eq!(line.chords[0].offset, 0); + assert_eq!(line.chords[1].offset, 8); + } + + #[test] + fn section_kind_from_label() { + assert_eq!(SectionKind::from_label("Chorus"), SectionKind::Chorus); + assert_eq!(SectionKind::from_label("Pre-Chorus"), SectionKind::PreChorus); + assert_eq!(SectionKind::from_label("Tab"), SectionKind::Tab); + assert_eq!(SectionKind::from_label("Riff"), SectionKind::Other("Riff".into())); + } +}