use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::Serialize;
#[non_exhaustive]
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCode {
StoreNotConfigured,
RunNotFound,
ContextIdRequired,
Unauthorized,
Forbidden,
Internal,
InvalidFilter,
}
#[derive(Debug, Serialize)]
struct ErrorBody {
error: ErrorCode,
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum OpsError {
#[error("backing store not configured for this endpoint")]
StoreNotConfigured,
#[error("run not found")]
RunNotFound,
#[error("context_id parameter is required")]
ContextIdRequired,
#[error("unauthorized")]
Unauthorized,
#[error("forbidden")]
Forbidden,
#[error("internal error")]
Internal(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("invalid filter value")]
InvalidFilter,
}
impl IntoResponse for OpsError {
fn into_response(self) -> Response {
let (status, code) = match self {
OpsError::StoreNotConfigured => {
(StatusCode::NOT_IMPLEMENTED, ErrorCode::StoreNotConfigured)
}
OpsError::RunNotFound => (StatusCode::NOT_FOUND, ErrorCode::RunNotFound),
OpsError::ContextIdRequired => (StatusCode::BAD_REQUEST, ErrorCode::ContextIdRequired),
OpsError::Unauthorized => (StatusCode::UNAUTHORIZED, ErrorCode::Unauthorized),
OpsError::Forbidden => (StatusCode::FORBIDDEN, ErrorCode::Forbidden),
OpsError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::Internal),
OpsError::InvalidFilter => (StatusCode::BAD_REQUEST, ErrorCode::InvalidFilter),
};
(status, Json(ErrorBody { error: code })).into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn store_not_configured_serialises_snake_case() {
let body = ErrorBody {
error: ErrorCode::StoreNotConfigured,
};
let json = serde_json::to_string(&body).unwrap();
assert!(json.contains("store_not_configured"), "got: {json}");
}
#[test]
fn internal_error_maps_to_500() {
use std::error::Error;
use std::io;
let err = io::Error::other("db timeout");
let ops_err = OpsError::Internal(Box::new(err));
assert!(ops_err.source().is_some(), "source chain must be retained");
assert!(ops_err.source().unwrap().to_string().contains("db timeout"));
let resp = ops_err.into_response();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
}