distri_types/
api_error.rs1use thiserror::Error;
15
16#[derive(Debug, Error)]
17pub enum ApiError {
18 #[error("{0}")]
21 BadRequest(String),
22
23 #[error("{0}")]
26 Unauthorized(String),
27
28 #[error("{0}")]
31 Forbidden(String),
32
33 #[error("{0}")]
35 NotFound(String),
36
37 #[error("{0}")]
40 Conflict(String),
41
42 #[error("{0}")]
46 Unprocessable(String),
47
48 #[error("{0}")]
51 ServiceUnavailable(String),
52
53 #[error("{0}")]
59 BadGateway(String),
60
61 #[error(transparent)]
65 Internal(#[from] anyhow::Error),
66}
67
68impl ApiError {
69 pub fn status(&self) -> u16 {
71 match self {
72 Self::BadRequest(_) => 400,
73 Self::Unauthorized(_) => 401,
74 Self::Forbidden(_) => 403,
75 Self::NotFound(_) => 404,
76 Self::Conflict(_) => 409,
77 Self::Unprocessable(_) => 422,
78 Self::ServiceUnavailable(_) => 503,
79 Self::BadGateway(_) => 502,
80 Self::Internal(_) => 500,
81 }
82 }
83
84 pub fn message(&self) -> String {
88 match self {
89 Self::Internal(_) => "internal server error".to_string(),
90 other => other.to_string(),
91 }
92 }
93}
94
95impl ApiError {
97 pub fn bad_request(msg: impl Into<String>) -> Self {
98 Self::BadRequest(msg.into())
99 }
100 pub fn unauthorized(msg: impl Into<String>) -> Self {
101 Self::Unauthorized(msg.into())
102 }
103 pub fn forbidden(msg: impl Into<String>) -> Self {
104 Self::Forbidden(msg.into())
105 }
106 pub fn not_found(msg: impl Into<String>) -> Self {
107 Self::NotFound(msg.into())
108 }
109 pub fn conflict(msg: impl Into<String>) -> Self {
110 Self::Conflict(msg.into())
111 }
112 pub fn unprocessable(msg: impl Into<String>) -> Self {
113 Self::Unprocessable(msg.into())
114 }
115 pub fn service_unavailable(msg: impl Into<String>) -> Self {
116 Self::ServiceUnavailable(msg.into())
117 }
118 pub fn bad_gateway(msg: impl Into<String>) -> Self {
119 Self::BadGateway(msg.into())
120 }
121}
122
123pub type ApiResult<T> = Result<T, ApiError>;
124
125#[cfg(feature = "actix")]
131impl actix_web::ResponseError for ApiError {
132 fn status_code(&self) -> actix_web::http::StatusCode {
133 actix_web::http::StatusCode::from_u16(self.status())
134 .unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR)
135 }
136
137 fn error_response(&self) -> actix_web::HttpResponse {
138 if let ApiError::Internal(e) = self {
139 tracing::error!("internal error: {:#}", e);
140 }
141 actix_web::HttpResponse::build(self.status_code())
142 .json(serde_json::json!({ "error": self.message() }))
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn status_codes() {
152 assert_eq!(ApiError::bad_request("x").status(), 400);
153 assert_eq!(ApiError::unauthorized("x").status(), 401);
154 assert_eq!(ApiError::forbidden("x").status(), 403);
155 assert_eq!(ApiError::not_found("x").status(), 404);
156 assert_eq!(ApiError::conflict("x").status(), 409);
157 assert_eq!(ApiError::unprocessable("x").status(), 422);
158 assert_eq!(ApiError::service_unavailable("x").status(), 503);
159 assert_eq!(ApiError::Internal(anyhow::anyhow!("oops")).status(), 500);
160 }
161
162 #[test]
163 fn internal_message_is_generic() {
164 let e = ApiError::Internal(anyhow::anyhow!("db failed: ..."));
165 assert_eq!(e.message(), "internal server error");
166 }
167
168 #[test]
169 fn anyhow_conversion_is_internal() {
170 let e: ApiError = anyhow::anyhow!("any error").into();
171 assert!(matches!(e, ApiError::Internal(_)));
172 }
173}