diff --git a/crates/domain/Cargo.toml b/crates/domain/Cargo.toml new file mode 100644 index 0000000..5cff72b --- /dev/null +++ b/crates/domain/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "domain" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +uuid = { workspace = true } +rand = { workspace = true } +serde = { workspace = true } +async-trait = { workspace = true } diff --git a/crates/domain/src/chord.rs b/crates/domain/src/chord.rs new file mode 100644 index 0000000..5182662 --- /dev/null +++ b/crates/domain/src/chord.rs @@ -0,0 +1,85 @@ +use serde::{Deserialize, Serialize}; +use crate::Note; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(into = "String", try_from = "String")] +pub struct Chord { + pub root: Note, + pub descriptor: Option, +} + +impl Chord { + pub fn from_str(s: &str) -> Option { + let (root, consumed) = Note::parse_prefix(s)?; + let descriptor = if consumed < s.len() { + Some(s[consumed..].to_string()) + } else { + None + }; + Some(Chord { root, descriptor }) + } + + /// Display chord name. use_sharps=true → "F#m", false → "Gbm". + pub fn name(&self, use_sharps: bool) -> String { + let root_str = if use_sharps { + self.root.to_sharp_str() + } else { + self.root.to_flat_str() + }; + match &self.descriptor { + Some(d) => format!("{}{}", root_str, d), + None => root_str.to_string(), + } + } +} + +impl From for String { + fn from(c: Chord) -> String { + c.name(true) + } +} + +impl TryFrom for Chord { + type Error = String; + fn try_from(s: String) -> Result { + Chord::from_str(&s).ok_or_else(|| format!("invalid chord: {}", s)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_simple() { + let c = Chord::from_str("Em").unwrap(); + assert_eq!(c.root, crate::Note::E); + assert_eq!(c.descriptor.as_deref(), Some("m")); + } + + #[test] + fn parse_no_descriptor() { + let c = Chord::from_str("G").unwrap(); + assert_eq!(c.root, crate::Note::G); + assert!(c.descriptor.is_none()); + } + + #[test] + fn parse_flat_root() { + let c = Chord::from_str("Bb").unwrap(); + assert_eq!(c.root, crate::Note::ASharpBFlat); + assert!(c.descriptor.is_none()); + } + + #[test] + fn name_sharp() { + let c = Chord { root: crate::Note::FSharpGFlat, descriptor: Some("m".into()) }; + assert_eq!(c.name(true), "F#m"); + } + + #[test] + fn name_flat() { + let c = Chord { root: crate::Note::ASharpBFlat, descriptor: None }; + assert_eq!(c.name(false), "Bb"); + } +} diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index dc9d2cf..3b1635d 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -1,3 +1,5 @@ pub mod note; +pub mod chord; pub use note::Note; +pub use chord::Chord;