Skip to main content

systemprompt_models/api/
errors.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5#[cfg(feature = "web")]
6use axum::http::StatusCode;
7#[cfg(feature = "web")]
8use axum::response::IntoResponse;
9#[cfg(feature = "web")]
10use axum::Json;
11
12#[derive(Debug, thiserror::Error)]
13pub enum InternalApiError {
14    #[error("Resource not found: {resource_type} with ID '{id}'")]
15    NotFound { resource_type: String, id: String },
16
17    #[error("Bad request: {message}")]
18    BadRequest { message: String },
19
20    #[error("Unauthorized access: {reason}")]
21    Unauthorized { reason: String },
22
23    #[error("Access forbidden: {resource} - {reason}")]
24    Forbidden { resource: String, reason: String },
25
26    #[error("Validation failed for field '{field}': {reason}")]
27    ValidationError { field: String, reason: String },
28
29    #[error("Conflict: {resource} already exists")]
30    ConflictError { resource: String },
31
32    #[error("Rate limit exceeded for {resource}")]
33    RateLimited { resource: String },
34
35    #[error("Service temporarily unavailable: {service}")]
36    ServiceUnavailable { service: String },
37
38    #[error("Database operation failed: {message}")]
39    DatabaseError { message: String },
40
41    #[error("JSON serialization failed")]
42    JsonError(#[from] serde_json::Error),
43
44    #[error("Authentication token error: {message}")]
45    AuthenticationError { message: String },
46
47    #[error("Internal server error: {message}")]
48    InternalError { message: String },
49}
50
51impl InternalApiError {
52    pub fn not_found(resource_type: impl Into<String>, id: impl Into<String>) -> Self {
53        Self::NotFound {
54            resource_type: resource_type.into(),
55            id: id.into(),
56        }
57    }
58
59    pub fn bad_request(message: impl Into<String>) -> Self {
60        Self::BadRequest {
61            message: message.into(),
62        }
63    }
64
65    pub fn unauthorized(reason: impl Into<String>) -> Self {
66        Self::Unauthorized {
67            reason: reason.into(),
68        }
69    }
70
71    pub fn forbidden(resource: impl Into<String>, reason: impl Into<String>) -> Self {
72        Self::Forbidden {
73            resource: resource.into(),
74            reason: reason.into(),
75        }
76    }
77
78    pub fn validation_error(field: impl Into<String>, reason: impl Into<String>) -> Self {
79        Self::ValidationError {
80            field: field.into(),
81            reason: reason.into(),
82        }
83    }
84
85    pub fn conflict(resource: impl Into<String>) -> Self {
86        Self::ConflictError {
87            resource: resource.into(),
88        }
89    }
90
91    pub fn rate_limited(resource: impl Into<String>) -> Self {
92        Self::RateLimited {
93            resource: resource.into(),
94        }
95    }
96
97    pub fn service_unavailable(service: impl Into<String>) -> Self {
98        Self::ServiceUnavailable {
99            service: service.into(),
100        }
101    }
102
103    pub fn internal_error(message: impl Into<String>) -> Self {
104        Self::InternalError {
105            message: message.into(),
106        }
107    }
108
109    pub fn database_error(message: impl Into<String>) -> Self {
110        Self::DatabaseError {
111            message: message.into(),
112        }
113    }
114
115    pub fn authentication_error(message: impl Into<String>) -> Self {
116        Self::AuthenticationError {
117            message: message.into(),
118        }
119    }
120
121    pub const fn error_code(&self) -> ErrorCode {
122        match self {
123            Self::NotFound { .. } => ErrorCode::NotFound,
124            Self::BadRequest { .. } => ErrorCode::BadRequest,
125            Self::Unauthorized { .. } => ErrorCode::Unauthorized,
126            Self::Forbidden { .. } => ErrorCode::Forbidden,
127            Self::ValidationError { .. } => ErrorCode::ValidationError,
128            Self::ConflictError { .. } => ErrorCode::ConflictError,
129            Self::RateLimited { .. } => ErrorCode::RateLimited,
130            Self::ServiceUnavailable { .. } => ErrorCode::ServiceUnavailable,
131            Self::DatabaseError { .. }
132            | Self::JsonError(_)
133            | Self::AuthenticationError { .. }
134            | Self::InternalError { .. } => ErrorCode::InternalError,
135        }
136    }
137}
138
139impl From<InternalApiError> for ApiError {
140    fn from(error: InternalApiError) -> Self {
141        let code = error.error_code();
142        let message = error.to_string();
143        let details = match &error {
144            InternalApiError::NotFound { resource_type, id } => Some(format!(
145                "The requested {resource_type} with ID '{id}' does not exist"
146            )),
147            InternalApiError::ValidationError { field, reason } => {
148                Some(format!("Field '{field}': {reason}"))
149            },
150            InternalApiError::Forbidden { resource, reason } => {
151                Some(format!("Access to {resource} denied: {reason}"))
152            },
153            InternalApiError::DatabaseError { message } => {
154                Some(format!("Database error: {message}"))
155            },
156            InternalApiError::JsonError(e) => Some(format!("JSON processing error: {e}")),
157            InternalApiError::AuthenticationError { message } => {
158                Some(format!("Authentication error: {message}"))
159            },
160            InternalApiError::BadRequest { .. }
161            | InternalApiError::Unauthorized { .. }
162            | InternalApiError::ConflictError { .. }
163            | InternalApiError::RateLimited { .. }
164            | InternalApiError::ServiceUnavailable { .. }
165            | InternalApiError::InternalError { .. } => None,
166        };
167
168        Self::new(code, message).with_details(details.unwrap_or_default())
169    }
170}
171
172#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
173#[serde(rename_all = "snake_case")]
174pub enum ErrorCode {
175    NotFound,
176    BadRequest,
177    Unauthorized,
178    Forbidden,
179    InternalError,
180    ValidationError,
181    ConflictError,
182    RateLimited,
183    ServiceUnavailable,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct ValidationError {
188    pub field: String,
189
190    pub message: String,
191
192    pub code: String,
193
194    pub context: Option<Value>,
195}
196
197#[derive(Debug, Serialize, Deserialize)]
198pub struct ApiError {
199    pub code: ErrorCode,
200
201    pub message: String,
202
203    pub details: Option<String>,
204
205    pub error_key: Option<String>,
206
207    pub path: Option<String>,
208
209    #[serde(default)]
210    pub validation_errors: Vec<ValidationError>,
211
212    pub timestamp: DateTime<Utc>,
213
214    pub trace_id: Option<String>,
215}
216
217impl ApiError {
218    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
219        Self {
220            code,
221            message: message.into(),
222            details: None,
223            error_key: None,
224            path: None,
225            validation_errors: Vec::new(),
226            timestamp: Utc::now(),
227            trace_id: None,
228        }
229    }
230
231    pub fn with_details(mut self, details: impl Into<String>) -> Self {
232        self.details = Some(details.into());
233        self
234    }
235
236    pub fn with_error_key(mut self, key: impl Into<String>) -> Self {
237        self.error_key = Some(key.into());
238        self
239    }
240
241    pub fn with_path(mut self, path: impl Into<String>) -> Self {
242        self.path = Some(path.into());
243        self
244    }
245
246    pub fn with_validation_errors(mut self, errors: Vec<ValidationError>) -> Self {
247        self.validation_errors = errors;
248        self
249    }
250
251    pub fn with_trace_id(mut self, id: impl Into<String>) -> Self {
252        self.trace_id = Some(id.into());
253        self
254    }
255
256    pub fn not_found(message: impl Into<String>) -> Self {
257        Self::new(ErrorCode::NotFound, message)
258    }
259
260    pub fn bad_request(message: impl Into<String>) -> Self {
261        Self::new(ErrorCode::BadRequest, message)
262    }
263
264    pub fn unauthorized(message: impl Into<String>) -> Self {
265        Self::new(ErrorCode::Unauthorized, message)
266    }
267
268    pub fn forbidden(message: impl Into<String>) -> Self {
269        Self::new(ErrorCode::Forbidden, message)
270    }
271
272    pub fn internal_error(message: impl Into<String>) -> Self {
273        Self::new(ErrorCode::InternalError, message)
274    }
275
276    pub fn validation_error(message: impl Into<String>, errors: Vec<ValidationError>) -> Self {
277        Self::new(ErrorCode::ValidationError, message).with_validation_errors(errors)
278    }
279
280    pub fn conflict(message: impl Into<String>) -> Self {
281        Self::new(ErrorCode::ConflictError, message)
282    }
283}
284
285#[derive(Debug, Serialize, Deserialize)]
286pub struct ErrorResponse {
287    pub error: ApiError,
288
289    pub api_version: String,
290}
291
292#[cfg(feature = "web")]
293impl ErrorCode {
294    pub const fn status_code(&self) -> StatusCode {
295        match self {
296            Self::NotFound => StatusCode::NOT_FOUND,
297            Self::BadRequest => StatusCode::BAD_REQUEST,
298            Self::Unauthorized => StatusCode::UNAUTHORIZED,
299            Self::Forbidden => StatusCode::FORBIDDEN,
300            Self::ValidationError => StatusCode::UNPROCESSABLE_ENTITY,
301            Self::ConflictError => StatusCode::CONFLICT,
302            Self::RateLimited => StatusCode::TOO_MANY_REQUESTS,
303            Self::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
304            Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
305        }
306    }
307}
308
309#[cfg(feature = "web")]
310impl IntoResponse for ApiError {
311    fn into_response(self) -> axum::response::Response {
312        let status = self.code.status_code();
313
314        if status.is_server_error() {
315            tracing::error!(
316                error_code = ?self.code,
317                message = %self.message,
318                path = ?self.path,
319                trace_id = ?self.trace_id,
320                "API server error response"
321            );
322        } else if status.is_client_error() {
323            tracing::warn!(
324                error_code = ?self.code,
325                message = %self.message,
326                path = ?self.path,
327                trace_id = ?self.trace_id,
328                "API client error response"
329            );
330        }
331
332        (status, Json(self)).into_response()
333    }
334}
335
336#[cfg(feature = "web")]
337impl IntoResponse for InternalApiError {
338    fn into_response(self) -> axum::response::Response {
339        let error: ApiError = self.into();
340        error.into_response()
341    }
342}