| |
| |
| |
| |
| |
|
|
| 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, DeliveryInsertionCandidateDto, DeliveryInsertionRequestDto, |
| DeliveryInsertionResponseDto, JobAnalysisDto, JobRoutesDto, JobSnapshotDto, JobSummaryDto, |
| PlanDto, |
| }; |
| use super::errors::{parse_job_id, status_from_routing_error, status_from_solver_error}; |
| use super::sse; |
| use crate::data::{generate, DemoData}; |
| use crate::domain::{build_routes_snapshot, prepare_plan, rank_delivery_insertions}; |
| use crate::solver::SolverService; |
|
|
| |
| 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() |
| } |
| } |
|
|
| |
| 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)) |
| .route( |
| "/recommendations/delivery-insertions", |
| post(recommend_delivery_insertions), |
| ) |
| .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: "SolverForge Deliveries", |
| version: env!("CARGO_PKG_VERSION"), |
| solver_engine: "SolverForge", |
| }) |
| } |
|
|
| |
| async fn list_demo_data() -> Json<Vec<&'static str>> { |
| Json(vec![ |
| DemoData::Philadelphia.id(), |
| DemoData::Hartford.id(), |
| DemoData::Firenze.id(), |
| ]) |
| } |
|
|
| |
| 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); |
| 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_plan(&mut plan) |
| .await |
| .map_err(status_from_routing_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 mut snapshot = state |
| .solver |
| .get_snapshot(&id, query.snapshot_revision) |
| .map_err(status_from_solver_error)?; |
| if snapshot |
| .solution |
| .vehicles |
| .iter() |
| .any(|vehicle| vehicle.prepared_routing.is_none()) |
| { |
| |
| |
| prepare_plan(&mut snapshot.solution) |
| .await |
| .map_err(status_from_routing_error)?; |
| } |
| let routes = build_routes_snapshot(&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) |
| } |
|
|
| |
| async fn recommend_delivery_insertions( |
| Json(request): Json<DeliveryInsertionRequestDto>, |
| ) -> Result<Json<DeliveryInsertionResponseDto>, StatusCode> { |
| let mut plan = request |
| .plan |
| .to_domain() |
| .map_err(|_| StatusCode::BAD_REQUEST)?; |
| if request.delivery_id >= plan.deliveries.len() { |
| return Err(StatusCode::BAD_REQUEST); |
| } |
| |
| |
| prepare_plan(&mut plan) |
| .await |
| .map_err(status_from_routing_error)?; |
| let candidates = rank_delivery_insertions( |
| &plan, |
| request.delivery_id, |
| request.limit.unwrap_or(8).min(24), |
| ) |
| .await |
| .map_err(status_from_routing_error)? |
| .into_iter() |
| .map(DeliveryInsertionCandidateDto::from_candidate) |
| .collect(); |
| Ok(Json(DeliveryInsertionResponseDto { |
| delivery_id: request.delivery_id, |
| candidates, |
| })) |
| } |
|
|