refactor: DDD/CQRS architecture, unified crate layout

- crates: common→application, api→presentation, infrastructure/*→adapters/*
- new crates: api-types, infra-wiring
- domain: errors/, models/, value_objects/, ports/, services/
- application: CQRS use cases (songs/, tabs/) w/ commands, queries, deps
- unified DomainError replaces RepositoryError
- workspace deps, unused dep cleanup
- fix: parse plain-text chord lines (UG drops spans mid-song)
- tests extracted to separate modules (tests/ dirs)
This commit is contained in:
2026-07-11 21:02:10 +02:00
parent a520251dab
commit d13df586dd
74 changed files with 1493 additions and 1076 deletions

View File

@@ -0,0 +1,50 @@
use super::Note;
use serde::{Deserialize, Serialize};
#[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 parse(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 })
}
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::parse(&s).ok_or_else(|| format!("invalid chord: {}", s))
}
}
#[cfg(test)]
#[path = "../tests/chord.rs"]
mod tests;

View File

@@ -0,0 +1,7 @@
mod chord;
mod note;
mod sorting;
pub use chord::*;
pub use note::*;
pub use sorting::*;

View File

@@ -0,0 +1,129 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Note {
A,
ASharpBFlat,
B,
C,
CSharpDFlat,
D,
DSharpEFlat,
E,
F,
FSharpGFlat,
G,
GSharpAFlat,
}
impl Note {
pub fn semitone(&self) -> u8 {
match self {
Note::C => 0,
Note::CSharpDFlat => 1,
Note::D => 2,
Note::DSharpEFlat => 3,
Note::E => 4,
Note::F => 5,
Note::FSharpGFlat => 6,
Note::G => 7,
Note::GSharpAFlat => 8,
Note::A => 9,
Note::ASharpBFlat => 10,
Note::B => 11,
}
}
pub fn from_semitone(s: u8) -> Note {
match s % 12 {
0 => Note::C,
1 => Note::CSharpDFlat,
2 => Note::D,
3 => Note::DSharpEFlat,
4 => Note::E,
5 => Note::F,
6 => Note::FSharpGFlat,
7 => Note::G,
8 => Note::GSharpAFlat,
9 => Note::A,
10 => Note::ASharpBFlat,
11 => Note::B,
_ => unreachable!(),
}
}
pub fn to_sharp_str(&self) -> &'static str {
match self {
Note::C => "C",
Note::CSharpDFlat => "C#",
Note::D => "D",
Note::DSharpEFlat => "D#",
Note::E => "E",
Note::F => "F",
Note::FSharpGFlat => "F#",
Note::G => "G",
Note::GSharpAFlat => "G#",
Note::A => "A",
Note::ASharpBFlat => "A#",
Note::B => "B",
}
}
pub fn to_flat_str(&self) -> &'static str {
match self {
Note::C => "C",
Note::CSharpDFlat => "Db",
Note::D => "D",
Note::DSharpEFlat => "Eb",
Note::E => "E",
Note::F => "F",
Note::FSharpGFlat => "Gb",
Note::G => "G",
Note::GSharpAFlat => "Ab",
Note::A => "A",
Note::ASharpBFlat => "Bb",
Note::B => "B",
}
}
pub fn parse_prefix(s: &str) -> Option<(Note, usize)> {
let mut chars = s.chars();
let root = match chars.next()? {
'A' => Note::A,
'B' => Note::B,
'C' => Note::C,
'D' => Note::D,
'E' => Note::E,
'F' => Note::F,
'G' => Note::G,
_ => return None,
};
match chars.next() {
Some('#') => Some((Self::sharp_of(root), 2)),
Some('b') if s.len() > 1 => {
let flatted = Self::flat_of(root)?;
Some((flatted, 2))
}
_ => Some((root, 1)),
}
}
pub fn parse(s: &str) -> Option<Note> {
let (note, consumed) = Self::parse_prefix(s)?;
if consumed == s.len() {
Some(note)
} else {
None
}
}
fn sharp_of(root: Note) -> Note {
Note::from_semitone((root.semitone() + 1) % 12)
}
fn flat_of(root: Note) -> Option<Note> {
Some(Note::from_semitone((root.semitone() + 11) % 12))
}
}
#[cfg(test)]
#[path = "../tests/note.rs"]
mod tests;

View File

@@ -0,0 +1,14 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortField {
#[default]
Date,
Title,
Artist,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortOrder {
#[default]
Desc,
Asc,
}