use af_context::RequestId;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde_json::json;
#[derive(Debug, Clone)]
pub struct ApiError {
pub status: StatusCode,
pub message: String,
pub request_id: Option<RequestId>,
}
impl ApiError {
pub fn new(status: StatusCode, message: impl Into<String>) -> Self {
Self {
status,
message: message.into(),
request_id: None,
}
}
pub fn localized(
status: StatusCode,
catalog: &af_i18n::I18n,
locale: &str,
key: &str,
args: &[(&str, &str)],
) -> Self {
Self::new(status, catalog.t(locale, key, args))
}
pub fn bad_request(message: impl Into<String>) -> Self {
Self::new(StatusCode::BAD_REQUEST, message)
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(StatusCode::NOT_FOUND, message)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(StatusCode::INTERNAL_SERVER_ERROR, message)
}
pub fn with_request_id(mut self, id: RequestId) -> Self {
self.request_id = Some(id);
self
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let body = json!({
"error": self.message,
"request_id": self.request_id,
});
(self.status, Json(body)).into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn localized_error_uses_catalog_fallback() {
let mut catalog = af_i18n::I18n::new("en").unwrap();
catalog
.load_locale("en", serde_json::json!({"error": "Unavailable"}))
.unwrap();
assert_eq!(
ApiError::localized(
StatusCode::SERVICE_UNAVAILABLE,
&catalog,
"fr",
"error",
&[]
)
.message,
"Unavailable"
);
}
}