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:
1
crates/application/.gitignore
vendored
Normal file
1
crates/application/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Cargo.lock
|
||||
8
crates/application/Cargo.toml
Normal file
8
crates/application/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "application"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
uuid = { workspace = true }
|
||||
domain = { workspace = true }
|
||||
2
crates/application/src/lib.rs
Normal file
2
crates/application/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod songs;
|
||||
pub mod tabs;
|
||||
17
crates/application/src/songs/commands.rs
Normal file
17
crates/application/src/songs/commands.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use domain::models::Song;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct SaveSongCommand {
|
||||
pub song: Song,
|
||||
}
|
||||
|
||||
pub struct DeleteSongCommand {
|
||||
pub id: Uuid,
|
||||
}
|
||||
|
||||
pub struct UpdateSongMetaCommand {
|
||||
pub id: Uuid,
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub original_key: Option<String>,
|
||||
}
|
||||
8
crates/application/src/songs/delete_song.rs
Normal file
8
crates/application/src/songs/delete_song.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use domain::errors::DomainError;
|
||||
|
||||
use super::commands::DeleteSongCommand;
|
||||
use super::deps::SongCommandDeps;
|
||||
|
||||
pub async fn execute(deps: &SongCommandDeps, cmd: DeleteSongCommand) -> Result<(), DomainError> {
|
||||
deps.repo.delete(cmd.id).await
|
||||
}
|
||||
11
crates/application/src/songs/deps.rs
Normal file
11
crates/application/src/songs/deps.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use domain::ports::{SongRepositoryPort, SongSearchPort};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct SongCommandDeps {
|
||||
pub repo: Arc<dyn SongRepositoryPort>,
|
||||
}
|
||||
|
||||
pub struct SongQueryDeps {
|
||||
pub repo: Arc<dyn SongRepositoryPort>,
|
||||
pub search: Arc<dyn SongSearchPort>,
|
||||
}
|
||||
12
crates/application/src/songs/get_song.rs
Normal file
12
crates/application/src/songs/get_song.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use domain::errors::DomainError;
|
||||
use domain::models::Song;
|
||||
|
||||
use super::deps::SongQueryDeps;
|
||||
use super::queries::GetSongQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &SongQueryDeps,
|
||||
query: GetSongQuery,
|
||||
) -> Result<Option<Song>, DomainError> {
|
||||
deps.repo.get(query.id).await
|
||||
}
|
||||
12
crates/application/src/songs/list_songs.rs
Normal file
12
crates/application/src/songs/list_songs.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use domain::errors::DomainError;
|
||||
use domain::models::SongSummary;
|
||||
|
||||
use super::deps::SongQueryDeps;
|
||||
use super::queries::ListSongsQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &SongQueryDeps,
|
||||
query: ListSongsQuery,
|
||||
) -> Result<Vec<SongSummary>, DomainError> {
|
||||
deps.repo.list(query.sort, query.order).await
|
||||
}
|
||||
9
crates/application/src/songs/mod.rs
Normal file
9
crates/application/src/songs/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod commands;
|
||||
pub mod delete_song;
|
||||
pub mod deps;
|
||||
pub mod get_song;
|
||||
pub mod list_songs;
|
||||
pub mod queries;
|
||||
pub mod save_song;
|
||||
pub mod search_songs;
|
||||
pub mod update_meta;
|
||||
17
crates/application/src/songs/queries.rs
Normal file
17
crates/application/src/songs/queries.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use domain::value_objects::{SortField, SortOrder};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct ListSongsQuery {
|
||||
pub sort: SortField,
|
||||
pub order: SortOrder,
|
||||
}
|
||||
|
||||
pub struct GetSongQuery {
|
||||
pub id: Uuid,
|
||||
}
|
||||
|
||||
pub struct SearchSongsQuery {
|
||||
pub query: String,
|
||||
pub sort: SortField,
|
||||
pub order: SortOrder,
|
||||
}
|
||||
12
crates/application/src/songs/save_song.rs
Normal file
12
crates/application/src/songs/save_song.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use domain::errors::DomainError;
|
||||
use domain::models::StoredSong;
|
||||
|
||||
use super::commands::SaveSongCommand;
|
||||
use super::deps::SongCommandDeps;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &SongCommandDeps,
|
||||
cmd: SaveSongCommand,
|
||||
) -> Result<StoredSong, DomainError> {
|
||||
deps.repo.save(&cmd.song).await
|
||||
}
|
||||
14
crates/application/src/songs/search_songs.rs
Normal file
14
crates/application/src/songs/search_songs.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use domain::errors::DomainError;
|
||||
use domain::models::SongSummary;
|
||||
|
||||
use super::deps::SongQueryDeps;
|
||||
use super::queries::SearchSongsQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &SongQueryDeps,
|
||||
query: SearchSongsQuery,
|
||||
) -> Result<Vec<SongSummary>, DomainError> {
|
||||
deps.search
|
||||
.search(&query.query, query.sort, query.order)
|
||||
.await
|
||||
}
|
||||
19
crates/application/src/songs/update_meta.rs
Normal file
19
crates/application/src/songs/update_meta.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use domain::errors::DomainError;
|
||||
use domain::models::SongSummary;
|
||||
|
||||
use super::commands::UpdateSongMetaCommand;
|
||||
use super::deps::SongCommandDeps;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &SongCommandDeps,
|
||||
cmd: UpdateSongMetaCommand,
|
||||
) -> Result<SongSummary, DomainError> {
|
||||
deps.repo
|
||||
.update_meta(
|
||||
cmd.id,
|
||||
cmd.title.as_deref(),
|
||||
cmd.artist.as_deref(),
|
||||
cmd.original_key.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
4
crates/application/src/tabs/commands.rs
Normal file
4
crates/application/src/tabs/commands.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub struct ParseTabCommand {
|
||||
pub source: Option<String>,
|
||||
pub html: Option<String>,
|
||||
}
|
||||
7
crates/application/src/tabs/deps.rs
Normal file
7
crates/application/src/tabs/deps.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use domain::ports::{TabFetcherPort, TabParserPort};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ParseTabDeps {
|
||||
pub fetcher: Arc<dyn TabFetcherPort>,
|
||||
pub parser: Arc<dyn TabParserPort>,
|
||||
}
|
||||
3
crates/application/src/tabs/mod.rs
Normal file
3
crates/application/src/tabs/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod commands;
|
||||
pub mod deps;
|
||||
pub mod parse_tab;
|
||||
33
crates/application/src/tabs/parse_tab.rs
Normal file
33
crates/application/src/tabs/parse_tab.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use domain::errors::DomainError;
|
||||
use domain::models::Song;
|
||||
use domain::ports::TabSource;
|
||||
|
||||
use super::commands::ParseTabCommand;
|
||||
use super::deps::ParseTabDeps;
|
||||
|
||||
pub async fn execute(deps: &ParseTabDeps, cmd: ParseTabCommand) -> Result<Song, DomainError> {
|
||||
let html = if let Some(raw_html) = cmd.html {
|
||||
Ok(raw_html)
|
||||
} else if let Some(source) = cmd.source {
|
||||
let tab_source = if source.starts_with("file://") {
|
||||
let path = source.trim_start_matches("file://");
|
||||
TabSource::File(PathBuf::from(path))
|
||||
} else {
|
||||
TabSource::Url(source)
|
||||
};
|
||||
deps.fetcher
|
||||
.fetch(tab_source)
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))
|
||||
} else {
|
||||
Err(DomainError::ValidationError(
|
||||
"Provide either 'source' or 'html'".into(),
|
||||
))
|
||||
}?;
|
||||
|
||||
deps.parser
|
||||
.parse(&html)
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))
|
||||
}
|
||||
Reference in New Issue
Block a user