Spaces:
Running
Running
| 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<AppState>) -> 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) | |
| } | |
| struct HealthResponse { | |
| status: &'static str, | |
| } | |
| async fn health() -> Json<HealthResponse> { | |
| Json(HealthResponse { status: "UP" }) | |
| } | |
| struct InfoResponse { | |
| name: &'static str, | |
| version: &'static str, | |
| solver_engine: &'static str, | |
| } | |
| async fn info() -> Json<InfoResponse> { | |
| Json(InfoResponse { | |
| name: env!("CARGO_PKG_NAME"), | |
| version: env!("CARGO_PKG_VERSION"), | |
| solver_engine: "SolverForge", | |
| }) | |
| } | |
| struct DemoDataCatalogResponse { | |
| default_id: &'static str, | |
| available_ids: Vec<&'static str>, | |
| } | |
| async fn list_demo_data() -> Json<DemoDataCatalogResponse> { | |
| 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<String>) -> Result<Json<PlanDto>, StatusCode> { | |
| let demo = id.parse::<DemoData>().map_err(|_| StatusCode::NOT_FOUND)?; | |
| let plan = generate(demo).await.map_err(status_from_demo_data_error)?; | |
| Ok(Json(PlanDto::from_plan(&plan))) | |
| } | |
| struct CreateJobResponse { | |
| id: String, | |
| } | |
| async fn create_job( | |
| State(state): State<Arc<AppState>>, | |
| Json(dto): Json<PlanDto>, | |
| ) -> Result<Json<CreateJobResponse>, 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<Arc<AppState>>, | |
| Path(id): Path<String>, | |
| ) -> Result<Json<JobSummaryDto>, 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<Arc<AppState>>, | |
| Path(id): Path<String>, | |
| ) -> Result<Json<JobSummaryDto>, StatusCode> { | |
| get_job(State(state), Path(id)).await | |
| } | |
| struct SnapshotQuery { | |
| snapshot_revision: Option<u64>, | |
| } | |
| async fn get_snapshot( | |
| State(state): State<Arc<AppState>>, | |
| Path(id): Path<String>, | |
| Query(query): Query<SnapshotQuery>, | |
| ) -> Result<Json<JobSnapshotDto>, 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<Arc<AppState>>, | |
| Path(id): Path<String>, | |
| Query(query): Query<SnapshotQuery>, | |
| ) -> Result<Json<JobAnalysisDto>, 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<Arc<AppState>>, | |
| Path(id): Path<String>, | |
| Query(query): Query<SnapshotQuery>, | |
| ) -> Result<Json<JobRoutesDto>, 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<Arc<AppState>>, | |
| Path(id): Path<String>, | |
| ) -> Result<StatusCode, StatusCode> { | |
| state.solver.pause(&id).map_err(status_from_solver_error)?; | |
| Ok(StatusCode::ACCEPTED) | |
| } | |
| async fn resume_job( | |
| State(state): State<Arc<AppState>>, | |
| Path(id): Path<String>, | |
| ) -> Result<StatusCode, StatusCode> { | |
| state.solver.resume(&id).map_err(status_from_solver_error)?; | |
| Ok(StatusCode::ACCEPTED) | |
| } | |
| async fn cancel_job( | |
| State(state): State<Arc<AppState>>, | |
| Path(id): Path<String>, | |
| ) -> Result<StatusCode, StatusCode> { | |
| state.solver.cancel(&id).map_err(status_from_solver_error)?; | |
| Ok(StatusCode::ACCEPTED) | |
| } | |
| async fn delete_job( | |
| State(state): State<Arc<AppState>>, | |
| Path(id): Path<String>, | |
| ) -> Result<StatusCode, StatusCode> { | |
| state.solver.delete(&id).map_err(status_from_solver_error)?; | |
| Ok(StatusCode::NO_CONTENT) | |
| } | |
| fn parse_job_id(id: &str) -> Result<usize, StatusCode> { | |
| id.parse::<usize>().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 | |
| } | |