Skip to main content

ai_memory/
errors.rs

1// Copyright 2026 AlphaOne LLC
2// SPDX-License-Identifier: Apache-2.0
3
4use 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    // -----------------------------------------------------------------
159    // W12-H — variant-by-variant display + into_response coverage
160    // -----------------------------------------------------------------
161
162    #[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}