feat(api): wire POST /tabs/parse endpoint with fetcher and parser

This commit is contained in:
2026-04-08 01:56:06 +02:00
parent fe7d2dbceb
commit 0444d11fb4
4 changed files with 93 additions and 0 deletions

27
crates/api/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "api"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = { workspace = true }
axum = { version = "0.8.8", features = ["macros"] }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tower-http = { version = "0.6.8", features = [
"cors",
"fs",
"trace",
"tracing",
] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
uuid = { workspace = true }
rand = { workspace = true }
persistence = { path = "../infrastructure/persistence" }
common = { path = "../common" }
domain = { path = "../domain" }
ug-parser = { path = "../infrastructure/ug-parser" }

24
crates/api/src/main.rs Normal file
View File

@@ -0,0 +1,24 @@
mod routes;
use axum::{routing::post, Router};
use routes::tabs::{parse_tab, AppState};
use std::sync::Arc;
use ug_parser::{UgHtmlParser, UgTabFetcher};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let state = Arc::new(AppState {
fetcher: Box::new(UgTabFetcher::new()),
parser: Box::new(UgHtmlParser),
});
let app = Router::new()
.route("/tabs/parse", post(parse_tab))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
tracing::info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}

View File

@@ -0,0 +1 @@
pub mod tabs;

View File

@@ -0,0 +1,41 @@
use axum::{extract::State, http::StatusCode, Json};
use domain::{TabFetcherPort, TabParserPort, TabSource};
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
pub struct AppState {
pub fetcher: Box<dyn TabFetcherPort>,
pub parser: Box<dyn TabParserPort>,
}
#[derive(Deserialize)]
pub struct ParseRequest {
pub source: String,
}
#[derive(Serialize)]
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 source = if body.source.starts_with("file://") {
let path = body.source.trim_start_matches("file://");
TabSource::File(PathBuf::from(path))
} else {
TabSource::Url(body.source)
};
let html = state.fetcher.fetch(source).await.map_err(|e| {
(StatusCode::BAD_GATEWAY, Json(ErrorResponse { error: e.to_string() }))
})?;
let song = state.parser.parse(&html).map_err(|e| {
(StatusCode::UNPROCESSABLE_ENTITY, Json(ErrorResponse { error: e.to_string() }))
})?;
Ok(Json(song))
}