1use axum::Json;
5use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use serde::Serialize;
8
9#[allow(dead_code)]
10#[derive(Debug, Serialize)]
11pub struct ApiError {
12 pub code: &'static str,
13 pub message: String,
14}
15
16#[allow(dead_code)]
17#[derive(Debug)]
18pub enum MemoryError {
19 NotFound(String),
20 ValidationFailed(String),
21 DatabaseError(String),
22 Conflict(String),
23}
24
25impl MemoryError {
26 pub fn code(&self) -> &'static str {
27 match self {
28 Self::NotFound(_) => "NOT_FOUND",
29 Self::ValidationFailed(_) => "VALIDATION_FAILED",
30 Self::DatabaseError(_) => "DATABASE_ERROR",
31 Self::Conflict(_) => "CONFLICT",
32 }
33 }
34
35 pub fn status(&self) -> StatusCode {
36 match self {
37 Self::NotFound(_) => StatusCode::NOT_FOUND,
38 Self::ValidationFailed(_) => StatusCode::BAD_REQUEST,
39 Self::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
40 Self::Conflict(_) => StatusCode::CONFLICT,
41 }
42 }
43
44 pub fn message(&self) -> &str {
45 match self {
46 Self::NotFound(m)
47 | Self::ValidationFailed(m)
48 | Self::DatabaseError(m)
49 | Self::Conflict(m) => m,
50 }
51 }
52}
53
54impl IntoResponse for MemoryError {
55 fn into_response(self) -> Response {
56 let body = ApiError {
57 code: self.code(),
58 message: self.message().to_string(),
59 };
60 (self.status(), Json(body)).into_response()
61 }
62}
63
64impl std::fmt::Display for MemoryError {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "[{}] {}", self.code(), self.message())
67 }
68}
69
70impl From<anyhow::Error> for MemoryError {
71 fn from(e: anyhow::Error) -> Self {
72 Self::DatabaseError(e.to_string())
73 }
74}
75
76impl From<rusqlite::Error> for MemoryError {
77 fn from(e: rusqlite::Error) -> Self {
78 Self::DatabaseError(e.to_string())
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn error_codes() {
88 assert_eq!(MemoryError::NotFound("x".into()).code(), "NOT_FOUND");
89 assert_eq!(
90 MemoryError::ValidationFailed("x".into()).code(),
91 "VALIDATION_FAILED"
92 );
93 assert_eq!(
94 MemoryError::DatabaseError("x".into()).code(),
95 "DATABASE_ERROR"
96 );
97 assert_eq!(MemoryError::Conflict("x".into()).code(), "CONFLICT");
98 }
99
100 #[test]
101 fn error_status_codes() {
102 assert_eq!(
103 MemoryError::NotFound("x".into()).status(),
104 StatusCode::NOT_FOUND
105 );
106 assert_eq!(
107 MemoryError::ValidationFailed("x".into()).status(),
108 StatusCode::BAD_REQUEST
109 );
110 assert_eq!(
111 MemoryError::DatabaseError("x".into()).status(),
112 StatusCode::INTERNAL_SERVER_ERROR
113 );
114 assert_eq!(
115 MemoryError::Conflict("x".into()).status(),
116 StatusCode::CONFLICT
117 );
118 }
119
120 #[test]
121 fn error_messages() {
122 assert_eq!(
123 MemoryError::NotFound("not here".into()).message(),
124 "not here"
125 );
126 assert_eq!(
127 MemoryError::ValidationFailed("bad input".into()).message(),
128 "bad input"
129 );
130 }
131
132 #[test]
133 fn error_display() {
134 let err = MemoryError::NotFound("memory xyz".into());
135 let display = format!("{err}");
136 assert!(display.contains("NOT_FOUND"));
137 assert!(display.contains("memory xyz"));
138 }
139
140 #[test]
141 fn from_anyhow() {
142 let err: MemoryError = anyhow::anyhow!("db broke").into();
143 assert_eq!(err.code(), "DATABASE_ERROR");
144 assert!(err.message().contains("db broke"));
145 }
146
147 #[test]
148 fn api_error_serializes() {
149 let api_err = ApiError {
150 code: "TEST",
151 message: "test msg".into(),
152 };
153 let json = serde_json::to_value(&api_err).unwrap();
154 assert_eq!(json["code"], "TEST");
155 assert_eq!(json["message"], "test msg");
156 }
157
158 #[test]
163 fn error_display_validation() {
164 let err = MemoryError::ValidationFailed("bad input".into());
165 let s = format!("{err}");
166 assert!(s.contains("VALIDATION_FAILED"));
167 assert!(s.contains("bad input"));
168 }
169
170 #[test]
171 fn error_display_database() {
172 let err = MemoryError::DatabaseError("conn refused".into());
173 let s = format!("{err}");
174 assert!(s.contains("DATABASE_ERROR"));
175 assert!(s.contains("conn refused"));
176 }
177
178 #[test]
179 fn error_display_conflict() {
180 let err = MemoryError::Conflict("dup".into());
181 let s = format!("{err}");
182 assert!(s.contains("CONFLICT"));
183 assert!(s.contains("dup"));
184 }
185
186 #[test]
187 fn error_message_database_and_conflict() {
188 assert_eq!(MemoryError::DatabaseError("oops".into()).message(), "oops");
189 assert_eq!(MemoryError::Conflict("c".into()).message(), "c");
190 }
191
192 #[test]
193 fn from_rusqlite_error_maps_to_database() {
194 let rusqlite_err = rusqlite::Error::InvalidQuery;
195 let err: MemoryError = rusqlite_err.into();
196 assert_eq!(err.code(), "DATABASE_ERROR");
197 }
198
199 #[test]
200 fn into_response_carries_status_and_body() {
201 use axum::response::IntoResponse;
202 let err = MemoryError::NotFound("missing".into());
203 let resp = err.into_response();
204 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
205 }
206
207 #[test]
208 fn into_response_validation_status() {
209 use axum::response::IntoResponse;
210 let err = MemoryError::ValidationFailed("v".into());
211 let resp = err.into_response();
212 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
213 }
214
215 #[test]
216 fn into_response_database_status() {
217 use axum::response::IntoResponse;
218 let err = MemoryError::DatabaseError("d".into());
219 let resp = err.into_response();
220 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
221 }
222
223 #[test]
224 fn into_response_conflict_status() {
225 use axum::response::IntoResponse;
226 let err = MemoryError::Conflict("c".into());
227 let resp = err.into_response();
228 assert_eq!(resp.status(), StatusCode::CONFLICT);
229 }
230}