1use axum::{
7 http::StatusCode,
8 response::{IntoResponse, Response},
9 Json,
10};
11use llm_registry_service::ServiceError;
12use serde::{Deserialize, Serialize};
13use std::fmt;
14
15#[derive(Debug)]
17pub struct ApiError {
18 status_code: StatusCode,
19 message: String,
20 error_code: Option<String>,
21}
22
23impl ApiError {
24 pub fn new(status_code: StatusCode, message: impl Into<String>) -> Self {
26 Self {
27 status_code,
28 message: message.into(),
29 error_code: None,
30 }
31 }
32
33 pub fn with_code(
35 status_code: StatusCode,
36 message: impl Into<String>,
37 error_code: impl Into<String>,
38 ) -> Self {
39 Self {
40 status_code,
41 message: message.into(),
42 error_code: Some(error_code.into()),
43 }
44 }
45
46 pub fn bad_request(message: impl Into<String>) -> Self {
48 Self::new(StatusCode::BAD_REQUEST, message)
49 }
50
51 pub fn not_found(message: impl Into<String>) -> Self {
53 Self::new(StatusCode::NOT_FOUND, message)
54 }
55
56 pub fn conflict(message: impl Into<String>) -> Self {
58 Self::new(StatusCode::CONFLICT, message)
59 }
60
61 pub fn unprocessable_entity(message: impl Into<String>) -> Self {
63 Self::new(StatusCode::UNPROCESSABLE_ENTITY, message)
64 }
65
66 pub fn internal_server_error(message: impl Into<String>) -> Self {
68 Self::new(StatusCode::INTERNAL_SERVER_ERROR, message)
69 }
70
71 pub fn unauthorized(message: impl Into<String>) -> Self {
73 Self::new(StatusCode::UNAUTHORIZED, message)
74 }
75
76 pub fn forbidden(message: impl Into<String>) -> Self {
78 Self::new(StatusCode::FORBIDDEN, message)
79 }
80}
81
82impl fmt::Display for ApiError {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 write!(f, "{}", self.message)
85 }
86}
87
88impl std::error::Error for ApiError {}
89
90#[derive(Debug, Serialize, Deserialize)]
92pub struct ErrorResponse {
93 pub status: u16,
95
96 pub error: String,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub code: Option<String>,
102
103 pub timestamp: chrono::DateTime<chrono::Utc>,
105}
106
107impl IntoResponse for ApiError {
108 fn into_response(self) -> Response {
109 let error_response = ErrorResponse {
110 status: self.status_code.as_u16(),
111 error: self.message,
112 code: self.error_code,
113 timestamp: chrono::Utc::now(),
114 };
115
116 (self.status_code, Json(error_response)).into_response()
117 }
118}
119
120impl From<ServiceError> for ApiError {
122 fn from(err: ServiceError) -> Self {
123 match err {
124 ServiceError::NotFound(msg) => {
125 ApiError::with_code(StatusCode::NOT_FOUND, msg, "NOT_FOUND")
126 }
127 ServiceError::AlreadyExists { name, version } => ApiError::with_code(
128 StatusCode::CONFLICT,
129 format!("Asset {}@{} already exists", name, version),
130 "ALREADY_EXISTS",
131 ),
132 ServiceError::ValidationFailed(msg) => ApiError::with_code(
133 StatusCode::UNPROCESSABLE_ENTITY,
134 format!("Validation failed: {}", msg),
135 "VALIDATION_FAILED",
136 ),
137 ServiceError::ChecksumVerificationFailed(msg) => ApiError::with_code(
138 StatusCode::UNPROCESSABLE_ENTITY,
139 format!("Checksum verification failed: {}", msg),
140 "CHECKSUM_MISMATCH",
141 ),
142 ServiceError::CircularDependency(msg) => ApiError::with_code(
143 StatusCode::UNPROCESSABLE_ENTITY,
144 format!("Circular dependency detected: {}", msg),
145 "CIRCULAR_DEPENDENCY",
146 ),
147 ServiceError::DependencyNotFound(msg) => ApiError::with_code(
148 StatusCode::UNPROCESSABLE_ENTITY,
149 format!("Dependency not found: {}", msg),
150 "DEPENDENCY_NOT_FOUND",
151 ),
152 ServiceError::VersionConflict(msg) => ApiError::with_code(
153 StatusCode::CONFLICT,
154 format!("Version conflict: {}", msg),
155 "VERSION_CONFLICT",
156 ),
157 ServiceError::PolicyValidationFailed {
158 policy_name,
159 message,
160 } => ApiError::with_code(
161 StatusCode::UNPROCESSABLE_ENTITY,
162 format!("Policy '{}' validation failed: {}", policy_name, message),
163 "POLICY_VALIDATION_FAILED",
164 ),
165 ServiceError::InvalidInput(msg) => {
166 ApiError::with_code(StatusCode::BAD_REQUEST, msg, "INVALID_INPUT")
167 }
168 ServiceError::NotPermitted(msg) => {
169 ApiError::with_code(StatusCode::FORBIDDEN, msg, "NOT_PERMITTED")
170 }
171 ServiceError::Database(msg) => ApiError::with_code(
172 StatusCode::INTERNAL_SERVER_ERROR,
173 format!("Database error: {}", msg),
174 "DATABASE_ERROR",
175 ),
176 ServiceError::Internal(msg) => ApiError::with_code(
177 StatusCode::INTERNAL_SERVER_ERROR,
178 format!("Internal error: {}", msg),
179 "INTERNAL_ERROR",
180 ),
181 }
182 }
183}
184
185impl From<serde_json::Error> for ApiError {
187 fn from(err: serde_json::Error) -> Self {
188 ApiError::bad_request(format!("Invalid JSON: {}", err))
189 }
190}
191
192impl From<std::io::Error> for ApiError {
193 fn from(err: std::io::Error) -> Self {
194 ApiError::internal_server_error(format!("I/O error: {}", err))
195 }
196}
197
198pub type ApiResult<T> = Result<T, ApiError>;
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_api_error_creation() {
207 let err = ApiError::bad_request("Invalid request");
208 assert_eq!(err.status_code, StatusCode::BAD_REQUEST);
209 assert_eq!(err.message, "Invalid request");
210 }
211
212 #[test]
213 fn test_service_error_conversion() {
214 let service_err = ServiceError::NotFound("asset-123".to_string());
215 let api_err: ApiError = service_err.into();
216 assert_eq!(api_err.status_code, StatusCode::NOT_FOUND);
217 }
218
219 #[test]
220 fn test_error_response_serialization() {
221 let response = ErrorResponse {
222 status: 404,
223 error: "Not found".to_string(),
224 code: Some("NOT_FOUND".to_string()),
225 timestamp: chrono::Utc::now(),
226 };
227
228 let json = serde_json::to_string(&response).unwrap();
229 assert!(json.contains("\"status\":404"));
230 assert!(json.contains("\"error\":\"Not found\""));
231 }
232}