1use crate::orm::DynError;
27use crate::orm::write::WriteError;
28use crate::web::{IntoResponse, Json, Response, StatusCode};
29
30#[derive(Debug)]
33pub enum ApiError {
34 NotFound(String),
36 BadRequest(String),
38 Validation(WriteError),
40 Database(sqlx::Error),
43 Internal(String),
45}
46
47impl ApiError {
48 pub fn not_found(msg: impl Into<String>) -> Self {
50 Self::NotFound(msg.into())
51 }
52 pub fn bad_request(msg: impl Into<String>) -> Self {
54 Self::BadRequest(msg.into())
55 }
56 pub fn internal(msg: impl Into<String>) -> Self {
58 Self::Internal(msg.into())
59 }
60}
61
62impl From<sqlx::Error> for ApiError {
63 fn from(e: sqlx::Error) -> Self {
64 Self::Database(e)
65 }
66}
67
68impl From<WriteError> for ApiError {
69 fn from(e: WriteError) -> Self {
70 if e.is_validation() {
74 return Self::Validation(e);
75 }
76 match e {
77 WriteError::Sqlx(s) => Self::Database(s),
78 other => Self::Internal(other.to_string()),
79 }
80 }
81}
82
83impl From<DynError> for ApiError {
84 fn from(e: DynError) -> Self {
85 match e {
86 DynError::Write(w) => Self::from(w),
87 DynError::Sqlx(s) => Self::from(s),
88 }
89 }
90}
91
92impl std::fmt::Display for ApiError {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 match self {
95 ApiError::NotFound(m) | ApiError::BadRequest(m) | ApiError::Internal(m) => {
96 write!(f, "{m}")
97 }
98 ApiError::Validation(e) => write!(f, "{e}"),
99 ApiError::Database(e) => write!(f, "database error: {e}"),
100 }
101 }
102}
103
104impl std::error::Error for ApiError {}
105
106fn json_error(status: StatusCode, code: &str, error: &str) -> Response {
108 (
109 status,
110 Json(serde_json::json!({ "error": error, "code": code })),
111 )
112 .into_response()
113}
114
115impl IntoResponse for ApiError {
116 fn into_response(self) -> Response {
117 match self {
118 ApiError::NotFound(msg) => json_error(StatusCode::NOT_FOUND, "not_found", &msg),
119 ApiError::BadRequest(msg) => json_error(StatusCode::BAD_REQUEST, "bad_request", &msg),
120 ApiError::Validation(e) => (
121 StatusCode::BAD_REQUEST,
122 Json(serde_json::json!({
123 "code": e.code(),
124 "field_errors": e.field_errors(),
125 "non_field_errors": e.non_field_errors(),
126 })),
127 )
128 .into_response(),
129 ApiError::Database(e) => {
130 tracing::error!(error = %e, "ApiError: database error");
131 json_error(
132 StatusCode::INTERNAL_SERVER_ERROR,
133 "database_error",
134 "internal server error",
135 )
136 }
137 ApiError::Internal(msg) => {
138 tracing::error!(detail = %msg, "ApiError: internal error");
139 json_error(
140 StatusCode::INTERNAL_SERVER_ERROR,
141 "internal_error",
142 "internal server error",
143 )
144 }
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::web::{IntoResponse, StatusCode};
153
154 #[test]
155 fn write_error_validation_becomes_a_400_with_the_field() {
156 let e = WriteError::RequiredFieldMissing {
157 field: "email".into(),
158 };
159 let api = ApiError::from(e);
160 assert_eq!(api.into_response().status(), StatusCode::BAD_REQUEST);
161 }
162
163 #[test]
164 fn a_bare_sqlx_error_becomes_an_opaque_500() {
165 let api = ApiError::from(sqlx::Error::RowNotFound);
166 assert_eq!(
167 api.into_response().status(),
168 StatusCode::INTERNAL_SERVER_ERROR
169 );
170 }
171
172 #[test]
173 fn dynerror_routes_write_to_400_and_sqlx_to_500() {
174 let v = ApiError::from(DynError::Write(WriteError::RequiredFieldMissing {
175 field: "x".into(),
176 }));
177 assert_eq!(v.into_response().status(), StatusCode::BAD_REQUEST);
178 let s = ApiError::from(DynError::Sqlx(sqlx::Error::RowNotFound));
179 assert_eq!(
180 s.into_response().status(),
181 StatusCode::INTERNAL_SERVER_ERROR
182 );
183 }
184
185 #[test]
186 fn explicit_constructors_carry_their_status() {
187 assert_eq!(
188 ApiError::not_found("nope").into_response().status(),
189 StatusCode::NOT_FOUND
190 );
191 assert_eq!(
192 ApiError::bad_request("bad").into_response().status(),
193 StatusCode::BAD_REQUEST
194 );
195 assert_eq!(
196 ApiError::internal("boom").into_response().status(),
197 StatusCode::INTERNAL_SERVER_ERROR
198 );
199 }
200}