solverforge-fsr / src /api /routes.rs
blackopsrepl's picture
feat(fsr): add snapshot-scoped route geometry
ae32abe
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)
}
#[derive(Serialize)]
struct HealthResponse {
status: &'static str,
}
async fn health() -> Json<HealthResponse> {
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<InfoResponse> {
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<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)))
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
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
}
#[derive(Debug, Default, Deserialize)]
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
}