use std::sync::Arc;
use axum::{extract::State, http::StatusCode, routing::get, Json, Router};
use rusqlite::OptionalExtension;
use super::sessions::ErrorResponse;
use crate::machine::MachineState;
type Refusal = (StatusCode, Json<ErrorResponse>);
fn refuse(status: StatusCode, error: impl std::fmt::Display) -> Refusal {
(status, Json(ErrorResponse { error: format!("{error:#}") }))
}
pub fn routes() -> Router<Arc<MachineState>> {
Router::new().route("/api/v1/workbench/layout", get(read).put(write))
}
pub const SELECT_LAYOUT: &str = "SELECT layout_json FROM workbench_layout WHERE id = 1";
pub const UPSERT_LAYOUT: &str = "INSERT INTO workbench_layout (id, layout_json, updated_at)
VALUES (1, ?1, datetime('now'))
ON CONFLICT(id) DO UPDATE SET
layout_json = excluded.layout_json,
updated_at = excluded.updated_at";
async fn read(State(machine): State<Arc<MachineState>>) -> Result<Json<serde_json::Value>, Refusal> {
let database = machine.database.clone();
let held = tokio::task::spawn_blocking(move || -> Result<Option<String>, String> {
let conn = database.get_connection().map_err(|e| format!("cannot open the workshop's database: {e}"))?;
conn.query_row(SELECT_LAYOUT, [], |row| row.get::<_, String>(0))
.optional()
.map_err(|e| format!("cannot read the saved workbench layout: {e}"))
})
.await
.map_err(|e| refuse(StatusCode::INTERNAL_SERVER_ERROR, e))?
.map_err(|e| refuse(StatusCode::INTERNAL_SERVER_ERROR, e))?;
let Some(raw) = held else {
return Err(refuse(StatusCode::NOT_FOUND, "no saved workbench layout"));
};
let layout = serde_json::from_str(&raw)
.map_err(|e| refuse(StatusCode::INTERNAL_SERVER_ERROR, format!("the saved workbench layout is not readable: {e}")))?;
Ok(Json(layout))
}
async fn write(
State(machine): State<Arc<MachineState>>,
Json(layout): Json<serde_json::Value>,
) -> Result<Json<serde_json::Value>, Refusal> {
if !layout.is_object() {
return Err(refuse(StatusCode::BAD_REQUEST, "a workbench layout is a JSON object"));
}
let json = serde_json::to_string(&layout).map_err(|e| refuse(StatusCode::INTERNAL_SERVER_ERROR, e))?;
let database = machine.database.clone();
tokio::task::spawn_blocking(move || -> Result<(), String> {
let conn = database.get_connection().map_err(|e| format!("cannot open the workshop's database: {e}"))?;
conn.execute(UPSERT_LAYOUT, [&json]).map_err(|e| format!("cannot save the workbench layout: {e}"))?;
Ok(())
})
.await
.map_err(|e| refuse(StatusCode::INTERNAL_SERVER_ERROR, e))?
.map_err(|e| refuse(StatusCode::INTERNAL_SERVER_ERROR, e))?;
Ok(Json(layout))
}
#[cfg(test)]
#[path = "workbench_layout_tests.rs"]
mod tests;