convergio_types/
api_error.rs1use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use serde::Serialize;
6
7#[derive(Debug, Clone, Serialize)]
9#[serde(tag = "error_kind")]
10pub enum ApiError {
11 BadRequest { message: String },
13 NotFound { message: String },
15 Unauthorized,
17 InternalError { message: String },
19 GateBlocked { gate: String, reason: String },
21}
22
23impl ApiError {
24 pub fn bad_request(msg: impl Into<String>) -> Self {
25 Self::BadRequest {
26 message: msg.into(),
27 }
28 }
29
30 pub fn not_found(msg: impl Into<String>) -> Self {
31 Self::NotFound {
32 message: msg.into(),
33 }
34 }
35
36 pub fn internal(msg: impl Into<String>) -> Self {
37 Self::InternalError {
38 message: msg.into(),
39 }
40 }
41
42 pub fn gate_blocked(gate: impl Into<String>, reason: impl Into<String>) -> Self {
43 Self::GateBlocked {
44 gate: gate.into(),
45 reason: reason.into(),
46 }
47 }
48
49 pub fn status_code(&self) -> StatusCode {
51 match self {
52 Self::BadRequest { .. } => StatusCode::BAD_REQUEST,
53 Self::NotFound { .. } => StatusCode::NOT_FOUND,
54 Self::Unauthorized => StatusCode::UNAUTHORIZED,
55 Self::InternalError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
56 Self::GateBlocked { .. } => StatusCode::UNPROCESSABLE_ENTITY,
57 }
58 }
59}
60
61impl std::fmt::Display for ApiError {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 Self::BadRequest { message } => write!(f, "bad request: {message}"),
65 Self::NotFound { message } => write!(f, "not found: {message}"),
66 Self::Unauthorized => write!(f, "unauthorized"),
67 Self::InternalError { message } => write!(f, "internal error: {message}"),
68 Self::GateBlocked { gate, reason } => {
69 write!(f, "gate blocked: {gate} — {reason}")
70 }
71 }
72 }
73}
74
75impl IntoResponse for ApiError {
76 fn into_response(self) -> Response {
77 let status = self.status_code();
78 let body = serde_json::json!({
79 "error": self.to_string(),
80 "status": status.as_u16(),
81 });
82 (status, axum::Json(body)).into_response()
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn bad_request_status() {
92 let err = ApiError::bad_request("missing field");
93 assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
94 }
95
96 #[test]
97 fn not_found_status() {
98 let err = ApiError::not_found("plan 99");
99 assert_eq!(err.status_code(), StatusCode::NOT_FOUND);
100 }
101
102 #[test]
103 fn unauthorized_status() {
104 assert_eq!(
105 ApiError::Unauthorized.status_code(),
106 StatusCode::UNAUTHORIZED
107 );
108 }
109
110 #[test]
111 fn internal_error_status() {
112 let err = ApiError::internal("db crash");
113 assert_eq!(err.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
114 }
115
116 #[test]
117 fn gate_blocked_status() {
118 let err = ApiError::gate_blocked("EvidenceGate", "no test_pass");
119 assert_eq!(err.status_code(), StatusCode::UNPROCESSABLE_ENTITY);
120 }
121
122 #[test]
123 fn display_formats_correctly() {
124 let err = ApiError::gate_blocked("Thor", "not validated");
125 let s = err.to_string();
126 assert!(s.contains("Thor"));
127 assert!(s.contains("not validated"));
128 }
129
130 #[test]
131 fn serializes_to_json() {
132 let err = ApiError::bad_request("oops");
133 let json = serde_json::to_value(&err).expect("serialize");
134 assert_eq!(json["error_kind"], "BadRequest");
135 assert_eq!(json["message"], "oops");
136 }
137}