llm_registry_api/
error.rs

1//! API error handling
2//!
3//! This module converts service errors into HTTP responses with appropriate
4//! status codes and error messages.
5
6use 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/// API error type that can be converted to HTTP responses
16#[derive(Debug)]
17pub struct ApiError {
18    status_code: StatusCode,
19    message: String,
20    error_code: Option<String>,
21}
22
23impl ApiError {
24    /// Create a new API error
25    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    /// Create an API error with an error code
34    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    /// Create a bad request error (400)
47    pub fn bad_request(message: impl Into<String>) -> Self {
48        Self::new(StatusCode::BAD_REQUEST, message)
49    }
50
51    /// Create a not found error (404)
52    pub fn not_found(message: impl Into<String>) -> Self {
53        Self::new(StatusCode::NOT_FOUND, message)
54    }
55
56    /// Create a conflict error (409)
57    pub fn conflict(message: impl Into<String>) -> Self {
58        Self::new(StatusCode::CONFLICT, message)
59    }
60
61    /// Create an unprocessable entity error (422)
62    pub fn unprocessable_entity(message: impl Into<String>) -> Self {
63        Self::new(StatusCode::UNPROCESSABLE_ENTITY, message)
64    }
65
66    /// Create an internal server error (500)
67    pub fn internal_server_error(message: impl Into<String>) -> Self {
68        Self::new(StatusCode::INTERNAL_SERVER_ERROR, message)
69    }
70
71    /// Create an unauthorized error (401)
72    pub fn unauthorized(message: impl Into<String>) -> Self {
73        Self::new(StatusCode::UNAUTHORIZED, message)
74    }
75
76    /// Create a forbidden error (403)
77    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/// Error response JSON structure
91#[derive(Debug, Serialize, Deserialize)]
92pub struct ErrorResponse {
93    /// HTTP status code
94    pub status: u16,
95
96    /// Error message
97    pub error: String,
98
99    /// Optional error code for programmatic handling
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub code: Option<String>,
102
103    /// Timestamp of the error
104    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
120/// Convert ServiceError to ApiError
121impl 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
185/// Convert common errors to ApiError
186impl 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
198/// Result type for API handlers
199pub 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}