ai_model_vault/api/
error.rs1use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use serde::Serialize;
6
7use crate::error::VaultError;
8
9#[derive(Debug, Serialize)]
11pub struct ApiErrorBody {
12 pub error: String,
13 pub code: u16,
14}
15
16#[derive(Debug)]
18pub struct ApiError {
19 pub status: StatusCode,
20 pub message: String,
21}
22
23impl ApiError {
24 pub fn bad_request(msg: impl Into<String>) -> Self {
25 Self {
26 status: StatusCode::BAD_REQUEST,
27 message: msg.into(),
28 }
29 }
30
31 pub fn not_found(msg: impl Into<String>) -> Self {
32 Self {
33 status: StatusCode::NOT_FOUND,
34 message: msg.into(),
35 }
36 }
37
38 pub fn unauthorized(msg: impl Into<String>) -> Self {
39 Self {
40 status: StatusCode::UNAUTHORIZED,
41 message: msg.into(),
42 }
43 }
44
45 pub fn internal(msg: impl Into<String>) -> Self {
46 Self {
47 status: StatusCode::INTERNAL_SERVER_ERROR,
48 message: msg.into(),
49 }
50 }
51
52 pub fn conflict(msg: impl Into<String>) -> Self {
53 Self {
54 status: StatusCode::CONFLICT,
55 message: msg.into(),
56 }
57 }
58
59 pub fn rate_limited(msg: impl Into<String>) -> Self {
60 Self {
61 status: StatusCode::TOO_MANY_REQUESTS,
62 message: msg.into(),
63 }
64 }
65}
66
67impl IntoResponse for ApiError {
68 fn into_response(self) -> Response {
69 let body = ApiErrorBody {
70 error: self.message,
71 code: self.status.as_u16(),
72 };
73 (self.status, axum::Json(body)).into_response()
74 }
75}
76
77impl From<VaultError> for ApiError {
78 fn from(err: VaultError) -> Self {
79 match &err {
80 VaultError::ModelNotFound(_)
81 | VaultError::VersionNotFound(_, _)
82 | VaultError::NotFound(_) => ApiError::not_found(err.to_string()),
83 VaultError::AuthenticationFailed => ApiError::unauthorized(err.to_string()),
84 VaultError::SecurityViolation(_) => ApiError::unauthorized("Access denied"),
85 VaultError::InvalidInput(_) | VaultError::UnsupportedFormat(_) => {
86 ApiError::bad_request(err.to_string())
87 }
88 _ => {
90 tracing::error!("Internal error: {err}");
91 ApiError::internal("An internal error occurred")
92 }
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn test_bad_request() {
103 let err = ApiError::bad_request("oops");
104 assert_eq!(err.status, StatusCode::BAD_REQUEST);
105 assert_eq!(err.message, "oops");
106 }
107
108 #[test]
109 fn test_not_found() {
110 let err = ApiError::not_found("missing");
111 assert_eq!(err.status, StatusCode::NOT_FOUND);
112 assert_eq!(err.message, "missing");
113 }
114
115 #[test]
116 fn test_unauthorized() {
117 let err = ApiError::unauthorized("denied");
118 assert_eq!(err.status, StatusCode::UNAUTHORIZED);
119 assert_eq!(err.message, "denied");
120 }
121
122 #[test]
123 fn test_internal() {
124 let err = ApiError::internal("boom");
125 assert_eq!(err.status, StatusCode::INTERNAL_SERVER_ERROR);
126 assert_eq!(err.message, "boom");
127 }
128
129 #[test]
130 fn test_conflict() {
131 let err = ApiError::conflict("exists");
132 assert_eq!(err.status, StatusCode::CONFLICT);
133 assert_eq!(err.message, "exists");
134 }
135
136 #[test]
137 fn test_rate_limited() {
138 let err = ApiError::rate_limited("slow down");
139 assert_eq!(err.status, StatusCode::TOO_MANY_REQUESTS);
140 assert_eq!(err.message, "slow down");
141 }
142
143 #[test]
144 fn test_into_response() {
145 let err = ApiError::bad_request("test error");
146 let response = err.into_response();
147 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
148 }
149
150 #[test]
151 fn test_from_vault_error_model_not_found() {
152 let err: ApiError = VaultError::ModelNotFound("m1".into()).into();
153 assert_eq!(err.status, StatusCode::NOT_FOUND);
154 }
155
156 #[test]
157 fn test_from_vault_error_version_not_found() {
158 let err: ApiError = VaultError::VersionNotFound(1, "m1".into()).into();
159 assert_eq!(err.status, StatusCode::NOT_FOUND);
160 }
161
162 #[test]
163 fn test_from_vault_error_auth_failed() {
164 let err: ApiError = VaultError::AuthenticationFailed.into();
165 assert_eq!(err.status, StatusCode::UNAUTHORIZED);
166 }
167
168 #[test]
172 fn test_all_not_found_variants_map_to_404() {
173 for err in [
174 VaultError::ModelNotFound("m".into()),
175 VaultError::VersionNotFound(3, "m".into()),
176 VaultError::NotFound("profile 'p'".into()),
177 ] {
178 let text = err.to_string();
179 let api: ApiError = err.into();
180 assert_eq!(api.status, StatusCode::NOT_FOUND, "{text} must be 404");
181 }
182 }
183
184 #[test]
185 fn test_from_vault_error_security_violation() {
186 let err: ApiError = VaultError::SecurityViolation("bad".into()).into();
187 assert_eq!(err.status, StatusCode::UNAUTHORIZED);
188 }
189
190 #[test]
191 fn test_from_vault_error_invalid_input() {
192 let err: ApiError = VaultError::InvalidInput("bad input".into()).into();
193 assert_eq!(err.status, StatusCode::BAD_REQUEST);
194 }
195
196 #[test]
197 fn test_from_vault_error_unsupported_format() {
198 let err: ApiError = VaultError::UnsupportedFormat("xyz".into()).into();
199 assert_eq!(err.status, StatusCode::BAD_REQUEST);
200 }
201
202 #[test]
203 fn test_from_vault_error_internal() {
204 let err: ApiError = VaultError::CryptoError("crypto fail".into()).into();
205 assert_eq!(err.status, StatusCode::INTERNAL_SERVER_ERROR);
206 assert_eq!(err.message, "An internal error occurred");
207 }
208
209 #[test]
210 fn test_api_error_body_serialization() {
211 let body = ApiErrorBody {
212 error: "not found".into(),
213 code: 404,
214 };
215 let json = serde_json::to_string(&body).unwrap();
216 assert!(json.contains("not found"));
217 assert!(json.contains("404"));
218 }
219}