1use af_context::RequestId;
9use axum::http::StatusCode;
10use axum::response::{IntoResponse, Response};
11use axum::Json;
12use serde_json::json;
13
14#[derive(Debug, Clone)]
16pub struct ApiError {
17 pub status: StatusCode,
19 pub message: String,
21 pub request_id: Option<RequestId>,
23}
24
25impl ApiError {
26 pub fn new(status: StatusCode, message: impl Into<String>) -> Self {
28 Self {
29 status,
30 message: message.into(),
31 request_id: None,
32 }
33 }
34
35 pub fn localized(
37 status: StatusCode,
38 catalog: &af_i18n::I18n,
39 locale: &str,
40 key: &str,
41 args: &[(&str, &str)],
42 ) -> Self {
43 Self::new(status, catalog.t(locale, key, args))
44 }
45
46 pub fn bad_request(message: impl Into<String>) -> Self {
48 Self::new(StatusCode::BAD_REQUEST, message)
49 }
50 pub fn not_found(message: impl Into<String>) -> Self {
52 Self::new(StatusCode::NOT_FOUND, message)
53 }
54 pub fn internal(message: impl Into<String>) -> Self {
56 Self::new(StatusCode::INTERNAL_SERVER_ERROR, message)
57 }
58
59 pub fn with_request_id(mut self, id: RequestId) -> Self {
61 self.request_id = Some(id);
62 self
63 }
64}
65
66impl IntoResponse for ApiError {
67 fn into_response(self) -> Response {
68 let body = json!({
69 "error": self.message,
70 "request_id": self.request_id,
71 });
72 (self.status, Json(body)).into_response()
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn localized_error_uses_catalog_fallback() {
82 let mut catalog = af_i18n::I18n::new("en").unwrap();
83 catalog
84 .load_locale("en", serde_json::json!({"error": "Unavailable"}))
85 .unwrap();
86 assert_eq!(
87 ApiError::localized(
88 StatusCode::SERVICE_UNAVAILABLE,
89 &catalog,
90 "fr",
91 "error",
92 &[]
93 )
94 .message,
95 "Unavailable"
96 );
97 }
98}