use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; use serde::Serialize; #[derive(Debug, Serialize)] struct ErrorResponse { error: String, } pub struct ApiError(pub pinakes_core::error::PinakesError); impl IntoResponse for ApiError { fn into_response(self) -> Response { use pinakes_core::error::PinakesError; let (status, message) = match &self.0 { PinakesError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), PinakesError::FileNotFound(path) => { // Only expose the file name, not the full path let name = path.file_name().map_or_else( || "unknown".to_string(), |n| n.to_string_lossy().to_string(), ); tracing::debug!(path = %path.display(), "file not found"); (StatusCode::NOT_FOUND, format!("file not found: {name}")) }, PinakesError::TagNotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), PinakesError::CollectionNotFound(msg) => { (StatusCode::NOT_FOUND, msg.clone()) }, PinakesError::DuplicateHash(msg) => (StatusCode::CONFLICT, msg.clone()), PinakesError::UnsupportedMediaType(path) => { let name = path.file_name().map_or_else( || "unknown".to_string(), |n| n.to_string_lossy().to_string(), ); ( StatusCode::BAD_REQUEST, format!("unsupported media type: {name}"), ) }, PinakesError::SearchParse(msg) => (StatusCode::BAD_REQUEST, msg.clone()), PinakesError::InvalidOperation(msg) => { (StatusCode::BAD_REQUEST, msg.clone()) }, PinakesError::Authentication(msg) => { (StatusCode::UNAUTHORIZED, msg.clone()) }, PinakesError::Authorization(msg) => (StatusCode::FORBIDDEN, msg.clone()), PinakesError::Serialization(msg) => { tracing::error!(error = %msg, "serialization error"); ( StatusCode::INTERNAL_SERVER_ERROR, "data serialization error".to_string(), ) }, PinakesError::Config(_) => { tracing::error!(error = %self.0, "configuration error"); ( StatusCode::INTERNAL_SERVER_ERROR, "internal configuration error".to_string(), ) }, _ => { tracing::error!(error = %self.0, "internal server error"); ( StatusCode::INTERNAL_SERVER_ERROR, "internal server error".to_string(), ) }, }; let body = serde_json::to_string(&ErrorResponse { error: message.clone(), }) .unwrap_or_else(|_| format!(r#"{{"error":"{message}"}}"#)); (status, [("content-type", "application/json")], body).into_response() } } impl From for ApiError { fn from(e: pinakes_core::error::PinakesError) -> Self { Self(e) } } impl ApiError { pub fn bad_request(msg: impl Into) -> Self { Self(pinakes_core::error::PinakesError::InvalidOperation( msg.into(), )) } pub fn not_found(msg: impl Into) -> Self { Self(pinakes_core::error::PinakesError::NotFound(msg.into())) } pub fn internal(msg: impl Into) -> Self { Self(pinakes_core::error::PinakesError::Database(msg.into())) } pub fn forbidden(msg: impl Into) -> Self { Self(pinakes_core::error::PinakesError::Authorization(msg.into())) } pub fn unauthorized(msg: impl Into) -> Self { Self(pinakes_core::error::PinakesError::Authentication( msg.into(), )) } } pub type ApiResult = Result;