use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use axum::extract::rejection::JsonRejection;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::{Json, Router, extract::State, routing::post};
use serde::{Deserialize, Serialize};
use crate::{BackendError, IpcBackend};
pub struct AppState {
backend: Mutex<Box<dyn IpcBackend>>,
initialized: AtomicBool,
}
impl AppState {
pub fn new(backend: Box<dyn IpcBackend>) -> Self {
Self {
backend: Mutex::new(backend),
initialized: AtomicBool::new(false),
}
}
pub fn lock_backend(&self) -> Result<MutexGuard<'_, Box<dyn IpcBackend>>, ApiError> {
self.backend
.lock()
.map_err(|_| ApiError::internal("backend mutex poisoned"))
}
}
pub fn router(backend: Box<dyn IpcBackend>) -> Router {
Router::new()
.route("/initialize", post(initialize))
.route("/process", post(process))
.route("/save_state", post(save_state))
.route("/reset", post(reset))
.with_state(Arc::new(AppState::new(backend)))
}
#[derive(Debug, Deserialize)]
struct InitializeReq {
model_path: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ProcessReq {
inputs: Vec<f32>,
}
#[derive(Debug, Deserialize)]
struct SaveStateReq {
model_path: String,
}
#[derive(Debug, Serialize)]
struct AckRes {
ok: bool,
message: &'static str,
}
#[derive(Debug, Serialize)]
struct ProcessRes {
ok: bool,
output: Vec<f32>,
}
#[derive(Debug, Serialize)]
struct ErrorRes {
ok: bool,
error: ErrorInfo,
}
#[derive(Debug, Serialize)]
struct ErrorInfo {
code: &'static str,
message: String,
}
#[derive(Debug)]
pub struct ApiError {
status: StatusCode,
code: &'static str,
message: String,
}
impl ApiError {
fn new(status: StatusCode, code: &'static str, message: impl Into<String>) -> Self {
Self {
status,
code,
message: message.into(),
}
}
fn internal(message: impl Into<String>) -> Self {
Self::new(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", message)
}
fn not_initialized() -> Self {
Self::new(
StatusCode::CONFLICT,
"not_initialized",
"backend not initialized — call POST /initialize first",
)
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(
self.status,
Json(ErrorRes {
ok: false,
error: ErrorInfo {
code: self.code,
message: self.message,
},
}),
)
.into_response()
}
}
fn json_rejection(rejection: JsonRejection) -> ApiError {
let (status, code) = match rejection {
JsonRejection::JsonSyntaxError(_) => (StatusCode::BAD_REQUEST, "bad_request"),
JsonRejection::MissingJsonContentType(_) => {
(StatusCode::UNSUPPORTED_MEDIA_TYPE, "unsupported_media_type")
}
JsonRejection::JsonDataError(_) => (StatusCode::UNPROCESSABLE_ENTITY, "invalid_input"),
_ => (StatusCode::BAD_REQUEST, "bad_request"),
};
ApiError::new(status, code, rejection.to_string())
}
fn backend_error(error: &BackendError) -> ApiError {
let (status, code) = match error {
BackendError::InvalidInput(_) => (StatusCode::UNPROCESSABLE_ENTITY, "invalid_input"),
_ => (StatusCode::BAD_GATEWAY, "backend_error"),
};
ApiError::new(status, code, error.to_string())
}
type ApiResult<T> = Result<T, ApiError>;
fn with_initialized_backend<R>(
state: &AppState,
f: impl FnOnce(&mut Box<dyn IpcBackend>) -> Result<R, BackendError>,
) -> ApiResult<R> {
let mut backend = state.lock_backend()?;
if !state.initialized.load(Ordering::SeqCst) {
return Err(ApiError::not_initialized());
}
f(&mut backend).map_err(|e| backend_error(&e))
}
async fn initialize(
State(state): State<Arc<AppState>>,
payload: Result<Json<InitializeReq>, JsonRejection>,
) -> ApiResult<Json<AckRes>> {
let Json(payload) = payload.map_err(json_rejection)?;
let mut backend = state.lock_backend()?;
backend
.initialize(payload.model_path.as_deref())
.map_err(|e| backend_error(&e))?;
state.initialized.store(true, Ordering::SeqCst);
Ok(Json(AckRes {
ok: true,
message: "initialized",
}))
}
async fn process(
State(state): State<Arc<AppState>>,
payload: Result<Json<ProcessReq>, JsonRejection>,
) -> ApiResult<Json<ProcessRes>> {
let Json(payload) = payload.map_err(json_rejection)?;
let output =
with_initialized_backend(&state, |backend| backend.process_batch(&payload.inputs))?;
Ok(Json(ProcessRes { ok: true, output }))
}
async fn save_state(
State(state): State<Arc<AppState>>,
payload: Result<Json<SaveStateReq>, JsonRejection>,
) -> ApiResult<Json<AckRes>> {
let Json(payload) = payload.map_err(json_rejection)?;
with_initialized_backend(&state, |backend| backend.save_state(&payload.model_path))?;
Ok(Json(AckRes {
ok: true,
message: "state saved",
}))
}
async fn reset(State(state): State<Arc<AppState>>) -> ApiResult<Json<AckRes>> {
let mut backend = state.lock_backend()?;
backend.reset().map_err(|e| backend_error(&e))?;
state.initialized.store(false, Ordering::SeqCst);
Ok(Json(AckRes {
ok: true,
message: "reset",
}))
}