heliosdb_nano/api/models/
error.rs1use axum::{
4 http::StatusCode,
5 response::{IntoResponse, Response},
6 Json,
7};
8use serde::{Deserialize, Serialize};
9
10use crate::Error;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ApiError {
15 #[serde(skip)]
17 pub status: StatusCode,
18
19 pub error: String,
21
22 pub message: String,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub details: Option<String>,
28}
29
30impl ApiError {
31 pub fn new(status: StatusCode, error: impl Into<String>, message: impl Into<String>) -> Self {
33 Self {
34 status,
35 error: error.into(),
36 message: message.into(),
37 details: None,
38 }
39 }
40
41 pub fn with_details(
43 status: StatusCode,
44 error: impl Into<String>,
45 message: impl Into<String>,
46 details: impl Into<String>,
47 ) -> Self {
48 Self {
49 status,
50 error: error.into(),
51 message: message.into(),
52 details: Some(details.into()),
53 }
54 }
55
56 pub fn bad_request(message: impl Into<String>) -> Self {
58 Self::new(StatusCode::BAD_REQUEST, "BadRequest", message)
59 }
60
61 pub fn not_found(message: impl Into<String>) -> Self {
63 Self::new(StatusCode::NOT_FOUND, "NotFound", message)
64 }
65
66 pub fn conflict(message: impl Into<String>) -> Self {
68 Self::new(StatusCode::CONFLICT, "Conflict", message)
69 }
70
71 pub fn internal_server_error(message: impl Into<String>) -> Self {
73 Self::new(StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", message)
74 }
75
76 pub fn internal(message: impl Into<String>) -> Self {
78 Self::internal_server_error(message)
79 }
80
81 pub fn unprocessable_entity(message: impl Into<String>) -> Self {
83 Self::new(StatusCode::UNPROCESSABLE_ENTITY, "UnprocessableEntity", message)
84 }
85
86 pub fn unauthorized(message: impl Into<String>) -> Self {
88 Self::new(StatusCode::UNAUTHORIZED, "Unauthorized", message)
89 }
90}
91
92impl From<Error> for ApiError {
94 fn from(err: Error) -> Self {
95 match err {
96 Error::Storage(msg) => {
97 if msg.contains("not found") || msg.contains("does not exist") {
98 ApiError::not_found(msg)
99 } else if msg.contains("already exists") {
100 ApiError::conflict(msg)
101 } else {
102 ApiError::internal_server_error(msg)
103 }
104 }
105 Error::SqlParse(msg) => ApiError::bad_request(msg),
106 Error::QueryExecution(msg) => ApiError::unprocessable_entity(msg),
107 Error::QueryTimeout(msg) => {
108 ApiError::new(StatusCode::REQUEST_TIMEOUT, "QueryTimeout", msg)
109 }
110 Error::QueryCancelled(msg) => {
111 ApiError::new(StatusCode::from_u16(499).unwrap_or(StatusCode::BAD_REQUEST), "QueryCancelled", msg)
112 }
113 Error::Transaction(msg) => ApiError::unprocessable_entity(msg),
114 Error::TypeConversion(msg) => ApiError::bad_request(msg),
115 Error::Config(msg) => ApiError::bad_request(msg),
116 Error::Protocol(msg) => ApiError::bad_request(msg),
117 Error::BranchMerge(msg) => ApiError::unprocessable_entity(msg),
118 Error::MergeConflict(msg) => ApiError::conflict(msg),
119 Error::ConstraintViolation(msg) => ApiError::conflict(msg),
120 Error::Encryption(_) | Error::VectorIndex(_) | Error::MultiTenant(_)
121 | Error::Audit(_) | Error::Compression(_) | Error::LockPoisoned(_)
122 | Error::Generic(_) => {
123 ApiError::internal_server_error(format!("{}", err))
124 }
125 Error::Io(e) => ApiError::internal_server_error(format!("I/O error: {}", e)),
126 }
127 }
128}
129
130impl IntoResponse for ApiError {
132 fn into_response(self) -> Response {
133 let status = self.status;
134 let body = Json(self);
135 (status, body).into_response()
136 }
137}
138
139#[cfg(test)]
140#[allow(clippy::unwrap_used, clippy::expect_used)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_api_error_creation() {
146 let err = ApiError::bad_request("Invalid input");
147 assert_eq!(err.status, StatusCode::BAD_REQUEST);
148 assert_eq!(err.error, "BadRequest");
149 assert_eq!(err.message, "Invalid input");
150 assert!(err.details.is_none());
151 }
152
153 #[test]
154 fn test_api_error_with_details() {
155 let err = ApiError::with_details(
156 StatusCode::BAD_REQUEST,
157 "ValidationError",
158 "Invalid branch name",
159 "Branch name must be alphanumeric",
160 );
161 assert_eq!(err.status, StatusCode::BAD_REQUEST);
162 assert_eq!(err.error, "ValidationError");
163 assert_eq!(err.message, "Invalid branch name");
164 assert_eq!(err.details, Some("Branch name must be alphanumeric".to_string()));
165 }
166
167 #[test]
168 fn test_error_conversion_storage_not_found() {
169 let domain_err = Error::storage("Branch 'dev' not found");
170 let api_err: ApiError = domain_err.into();
171 assert_eq!(api_err.status, StatusCode::NOT_FOUND);
172 }
173
174 #[test]
175 fn test_error_conversion_storage_exists() {
176 let domain_err = Error::storage("Branch already exists");
177 let api_err: ApiError = domain_err.into();
178 assert_eq!(api_err.status, StatusCode::CONFLICT);
179 }
180
181 #[test]
182 fn test_error_conversion_sql_parse() {
183 let domain_err = Error::sql_parse("Invalid SQL syntax");
184 let api_err: ApiError = domain_err.into();
185 assert_eq!(api_err.status, StatusCode::BAD_REQUEST);
186 }
187
188 #[test]
189 fn test_error_conversion_merge_conflict() {
190 let domain_err = Error::merge_conflict("Conflicts detected");
191 let api_err: ApiError = domain_err.into();
192 assert_eq!(api_err.status, StatusCode::CONFLICT);
193 }
194
195 #[test]
196 fn test_error_serialization() {
197 let err = ApiError::bad_request("Test error");
198 let json = serde_json::to_string(&err).unwrap();
199 let deserialized: ApiError = serde_json::from_str(&json).unwrap();
200 assert_eq!(deserialized.error, "BadRequest");
201 assert_eq!(deserialized.message, "Test error");
202 }
203}