feat(api): add song CRUD routes, wire SongService into AppState

This commit is contained in:
2026-04-08 03:07:28 +02:00
parent 0b47282547
commit 8c0824c67c
4 changed files with 101 additions and 16 deletions

View File

@@ -1 +1,2 @@
pub mod songs;
pub mod tabs;

View File

@@ -0,0 +1,70 @@
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use domain::RepositoryError;
use std::sync::Arc;
use uuid::Uuid;
use crate::routes::tabs::{AppState, ErrorResponse, ParseRequest, resolve_html};
pub async fn create_song(
State(state): State<Arc<AppState>>,
Json(body): Json<ParseRequest>,
) -> Result<Json<domain::StoredSong>, (StatusCode, Json<ErrorResponse>)> {
let html = resolve_html(&state, body).await.map_err(|e| {
(StatusCode::BAD_REQUEST, Json(ErrorResponse { error: e }))
})?;
let song = state.parser.parse(&html).map_err(|e| {
(StatusCode::UNPROCESSABLE_ENTITY, Json(ErrorResponse { error: e.to_string() }))
})?;
let stored = state.songs.save(&song).await.map_err(|e| {
(StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e.to_string() }))
})?;
Ok(Json(stored))
}
pub async fn list_songs(
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<domain::SongSummary>>, (StatusCode, Json<ErrorResponse>)> {
let songs = state.songs.list().await.map_err(|e| {
(StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e.to_string() }))
})?;
Ok(Json(songs))
}
pub async fn get_song(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<domain::Song>, (StatusCode, Json<ErrorResponse>)> {
let uuid = Uuid::parse_str(&id).map_err(|_| {
(StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "Invalid ID".into() }))
})?;
match state.songs.get(uuid).await {
Ok(Some(song)) => Ok(Json(song)),
Ok(None) => Err((StatusCode::NOT_FOUND, Json(ErrorResponse { error: "Not found".into() }))),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e.to_string() }))),
}
}
pub async fn delete_song(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let uuid = Uuid::parse_str(&id).map_err(|_| {
(StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "Invalid ID".into() }))
})?;
match state.songs.delete(uuid).await {
Ok(()) => Ok(StatusCode::NO_CONTENT),
Err(RepositoryError::NotFound) => {
Err((StatusCode::NOT_FOUND, Json(ErrorResponse { error: "Not found".into() })))
}
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e.to_string() }))),
}
}

View File

@@ -6,6 +6,7 @@ use std::{path::PathBuf, sync::Arc};
pub struct AppState {
pub fetcher: Box<dyn TabFetcherPort>,
pub parser: Box<dyn TabParserPort>,
pub songs: common::SongService,
}
#[derive(Deserialize)]
@@ -19,13 +20,9 @@ pub struct ErrorResponse {
pub error: String,
}
pub async fn parse_tab(
State(state): State<Arc<AppState>>,
Json(body): Json<ParseRequest>,
) -> Result<Json<domain::Song>, (StatusCode, Json<ErrorResponse>)> {
let html = if let Some(raw_html) = body.html {
// Raw HTML provided directly (e.g. from browser file upload via FileReader)
raw_html
pub async fn resolve_html(state: &AppState, body: ParseRequest) -> Result<String, String> {
if let Some(raw_html) = body.html {
Ok(raw_html)
} else if let Some(source) = body.source {
let tab_source = if source.starts_with("file://") {
let path = source.trim_start_matches("file://");
@@ -33,15 +30,19 @@ pub async fn parse_tab(
} else {
TabSource::Url(source)
};
state.fetcher.fetch(tab_source).await.map_err(|e| {
(StatusCode::BAD_GATEWAY, Json(ErrorResponse { error: e.to_string() }))
})?
state.fetcher.fetch(tab_source).await.map_err(|e| e.to_string())
} else {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse { error: "Provide either 'source' or 'html'".into() }),
));
};
Err("Provide either 'source' or 'html'".into())
}
}
pub async fn parse_tab(
State(state): State<Arc<AppState>>,
Json(body): Json<ParseRequest>,
) -> Result<Json<domain::Song>, (StatusCode, Json<ErrorResponse>)> {
let html = resolve_html(&state, body).await.map_err(|e| {
(StatusCode::BAD_REQUEST, Json(ErrorResponse { error: e }))
})?;
let song = state.parser.parse(&html).map_err(|e| {
(StatusCode::UNPROCESSABLE_ENTITY, Json(ErrorResponse { error: e.to_string() }))