From 0444d11fb4c573fed90da9b5f6fb81307d8f9b8d Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 8 Apr 2026 01:56:06 +0200 Subject: [PATCH] feat(api): wire POST /tabs/parse endpoint with fetcher and parser --- crates/api/Cargo.toml | 27 +++++++++++++++++++++++ crates/api/src/main.rs | 24 ++++++++++++++++++++ crates/api/src/routes/mod.rs | 1 + crates/api/src/routes/tabs.rs | 41 +++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 crates/api/Cargo.toml create mode 100644 crates/api/src/main.rs create mode 100644 crates/api/src/routes/mod.rs create mode 100644 crates/api/src/routes/tabs.rs diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml new file mode 100644 index 0000000..d72aad6 --- /dev/null +++ b/crates/api/Cargo.toml @@ -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" } diff --git a/crates/api/src/main.rs b/crates/api/src/main.rs new file mode 100644 index 0000000..6256efd --- /dev/null +++ b/crates/api/src/main.rs @@ -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(); +} diff --git a/crates/api/src/routes/mod.rs b/crates/api/src/routes/mod.rs new file mode 100644 index 0000000..4b2ad05 --- /dev/null +++ b/crates/api/src/routes/mod.rs @@ -0,0 +1 @@ +pub mod tabs; diff --git a/crates/api/src/routes/tabs.rs b/crates/api/src/routes/tabs.rs new file mode 100644 index 0000000..cf27a5e --- /dev/null +++ b/crates/api/src/routes/tabs.rs @@ -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, + pub parser: Box, +} + +#[derive(Deserialize)] +pub struct ParseRequest { + pub source: String, +} + +#[derive(Serialize)] +pub struct ErrorResponse { + pub error: String, +} + +pub async fn parse_tab( + State(state): State>, + Json(body): Json, +) -> Result, (StatusCode, Json)> { + 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)) +}