use axum::{ extract::{Path, Query, State}, http::StatusCode, routing::{get, post}, Json, Router, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; use super::dto::{analysis_response, JobAnalysisDto, JobSnapshotDto, JobSummaryDto, PlanDto}; use super::route_dto::JobRoutesDto; use super::route_geometry::{build_route_geometry, status_from_routing_error}; use super::sse; use crate::data::{generate, prepare_routing, DemoData, DemoDataError}; use crate::solver::SolverService; /// Shared application state. pub struct AppState { pub solver: SolverService, } impl AppState { pub fn new() -> Self { Self { solver: SolverService::new(), } } } impl Default for AppState { fn default() -> Self { Self::new() } } /// Creates the API router. pub fn router(state: Arc) -> Router { Router::new() .route("/health", get(health)) .route("/info", get(info)) .route("/demo-data", get(list_demo_data)) .route("/demo-data/{id}", get(get_demo_data)) .route("/jobs", post(create_job)) .route("/jobs/{id}", get(get_job).delete(delete_job)) .route("/jobs/{id}/status", get(get_job_status)) .route("/jobs/{id}/snapshot", get(get_snapshot)) .route("/jobs/{id}/analysis", get(analyze_by_id)) .route("/jobs/{id}/routes", get(get_routes)) .route("/jobs/{id}/pause", post(pause_job)) .route("/jobs/{id}/resume", post(resume_job)) .route("/jobs/{id}/cancel", post(cancel_job)) .route("/jobs/{id}/events", get(sse::events)) .with_state(state) } #[derive(Serialize)] struct HealthResponse { status: &'static str, } async fn health() -> Json { Json(HealthResponse { status: "UP" }) } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct InfoResponse { name: &'static str, version: &'static str, solver_engine: &'static str, } async fn info() -> Json { Json(InfoResponse { name: env!("CARGO_PKG_NAME"), version: env!("CARGO_PKG_VERSION"), solver_engine: "SolverForge", }) } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct DemoDataCatalogResponse { default_id: &'static str, available_ids: Vec<&'static str>, } async fn list_demo_data() -> Json { Json(DemoDataCatalogResponse { default_id: DemoData::default_demo_data().id(), available_ids: DemoData::available_demo_data() .iter() .map(|demo| demo.id()) .collect(), }) } async fn get_demo_data(Path(id): Path) -> Result, StatusCode> { let demo = id.parse::().map_err(|_| StatusCode::NOT_FOUND)?; let plan = generate(demo).await.map_err(status_from_demo_data_error)?; Ok(Json(PlanDto::from_plan(&plan))) } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct CreateJobResponse { id: String, } async fn create_job( State(state): State>, Json(dto): Json, ) -> Result, StatusCode> { let mut plan = dto.to_domain().map_err(|_| StatusCode::BAD_REQUEST)?; prepare_routing(&mut plan) .await .map_err(status_from_demo_data_error)?; let id = state .solver .start_job(plan) .map_err(status_from_solver_error)?; Ok(Json(CreateJobResponse { id })) } async fn get_job( State(state): State>, Path(id): Path, ) -> Result, StatusCode> { let job_id = parse_job_id(&id)?; let status = state .solver .get_status(&id) .map_err(status_from_solver_error)?; Ok(Json(JobSummaryDto::from_status(job_id, &status))) } async fn get_job_status( State(state): State>, Path(id): Path, ) -> Result, StatusCode> { get_job(State(state), Path(id)).await } #[derive(Debug, Default, Deserialize)] struct SnapshotQuery { snapshot_revision: Option, } async fn get_snapshot( State(state): State>, Path(id): Path, Query(query): Query, ) -> Result, StatusCode> { let snapshot = state .solver .get_snapshot(&id, query.snapshot_revision) .map_err(status_from_solver_error)?; Ok(Json(JobSnapshotDto::from_snapshot(&snapshot))) } async fn analyze_by_id( State(state): State>, Path(id): Path, Query(query): Query, ) -> Result, StatusCode> { let snapshot_analysis = state .solver .analyze_snapshot(&id, query.snapshot_revision) .map_err(status_from_solver_error)?; let analysis = analysis_response(&snapshot_analysis.analysis); Ok(Json(JobAnalysisDto::from_snapshot_analysis( &snapshot_analysis, analysis, ))) } async fn get_routes( State(state): State>, Path(id): Path, Query(query): Query, ) -> Result, StatusCode> { let job_id = parse_job_id(&id)?; let snapshot = state .solver .get_snapshot(&id, query.snapshot_revision) .map_err(status_from_solver_error)?; let routes = build_route_geometry(&snapshot.solution) .await .map_err(status_from_routing_error)?; Ok(Json(JobRoutesDto::new( job_id, snapshot.snapshot_revision, routes, ))) } async fn pause_job( State(state): State>, Path(id): Path, ) -> Result { state.solver.pause(&id).map_err(status_from_solver_error)?; Ok(StatusCode::ACCEPTED) } async fn resume_job( State(state): State>, Path(id): Path, ) -> Result { state.solver.resume(&id).map_err(status_from_solver_error)?; Ok(StatusCode::ACCEPTED) } async fn cancel_job( State(state): State>, Path(id): Path, ) -> Result { state.solver.cancel(&id).map_err(status_from_solver_error)?; Ok(StatusCode::ACCEPTED) } async fn delete_job( State(state): State>, Path(id): Path, ) -> Result { state.solver.delete(&id).map_err(status_from_solver_error)?; Ok(StatusCode::NO_CONTENT) } fn parse_job_id(id: &str) -> Result { id.parse::().map_err(|_| StatusCode::NOT_FOUND) } fn status_from_solver_error(error: solverforge::SolverManagerError) -> StatusCode { match error { solverforge::SolverManagerError::NoFreeJobSlots => StatusCode::SERVICE_UNAVAILABLE, solverforge::SolverManagerError::JobNotFound { .. } => StatusCode::NOT_FOUND, solverforge::SolverManagerError::InvalidStateTransition { .. } => StatusCode::CONFLICT, solverforge::SolverManagerError::NoSnapshotAvailable { .. } => StatusCode::CONFLICT, solverforge::SolverManagerError::SnapshotNotFound { .. } => StatusCode::NOT_FOUND, } } fn status_from_demo_data_error(error: DemoDataError) -> StatusCode { eprintln!("{error}"); StatusCode::SERVICE_UNAVAILABLE }