feat(domain): add Chord struct with parsing and display
This commit is contained in:
13
crates/domain/Cargo.toml
Normal file
13
crates/domain/Cargo.toml
Normal file
@@ -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 }
|
||||
85
crates/domain/src/chord.rs
Normal file
85
crates/domain/src/chord.rs
Normal file
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl Chord {
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
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<Chord> for String {
|
||||
fn from(c: Chord) -> String {
|
||||
c.name(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Chord {
|
||||
type Error = String;
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod note;
|
||||
pub mod chord;
|
||||
|
||||
pub use note::Note;
|
||||
pub use chord::Chord;
|
||||
|
||||
Reference in New Issue
Block a user