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 } => Some(format!("Database error: {message}")),
154            InternalApiError::JsonError(e) => Some(format!("JSON processing error: {e}")),
155            InternalApiError::AuthenticationError { message } => {
156                Some(format!("Authentication error: {message}"))
157            },
158            InternalApiError::BadRequest { .. }
159            | InternalApiError::Unauthorized { .. }
160            | InternalApiError::ConflictError { .. }
161            | InternalApiError::RateLimited { .. }
162            | InternalApiError::ServiceUnavailable { .. }
163            | InternalApiError::InternalError { .. } => None,
164        };
165
166        Self::new(code, message).with_details(details.unwrap_or_default())
167    }
168}
169
170#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum ErrorCode {
173    NotFound,
174    BadRequest,
175    Unauthorized,
176    Forbidden,
177    InternalError,
178    ValidationError,
179    ConflictError,
180    RateLimited,
181    ServiceUnavailable,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ValidationError {
186    pub field: String,
187
188    pub message: String,
189
190    pub code: String,
191
192    pub context: Option<Value>,
193}
194
195#[derive(Debug, Serialize, Deserialize)]
196pub struct ApiError {
197    pub code: ErrorCode,
198
199    pub message: String,
200
201    pub details: Option<String>,
202
203    pub error_key: Option<String>,
204
205    pub path: Option<String>,
206
207    #[serde(default)]
208    pub validation_errors: Vec<ValidationError>,
209
210    pub timestamp: DateTime<Utc>,
211
212    pub trace_id: Option<String>,
213}
214
215impl ApiError {
216    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
217        Self {
218            code,
219            message: message.into(),
220            details: None,
221            error_key: None,
222            path: None,
223            validation_errors: Vec::new(),
224            timestamp: Utc::now(),
225            trace_id: None,
226        }
227    }
228
229    pub fn with_details(mut self, details: impl Into<String>) -> Self {
230        self.details = Some(details.into());
231        self
232    }
233
234    pub fn with_error_key(mut self, key: impl Into<String>) -> Self {
235        self.error_key = Some(key.into());
236        self
237    }
238
239    pub fn with_path(mut self, path: impl Into<String>) -> Self {
240        self.path = Some(path.into());
241        self
242    }
243
244    pub fn with_validation_errors(mut self, errors: Vec<ValidationError>) -> Self {
245        self.validation_errors = errors;
246        self
247    }
248
249    pub fn with_trace_id(mut self, id: impl Into<String>) -> Self {
250        self.trace_id = Some(id.into());
251        self
252    }
253
254    pub fn not_found(message: impl Into<String>) -> Self {
255        Self::new(ErrorCode::NotFound, message)
256    }
257
258    pub fn bad_request(message: impl Into<String>) -> Self {
259        Self::new(ErrorCode::BadRequest, message)
260    }
261
262    pub fn unauthorized(message: impl Into<String>) -> Self {
263        Self::new(ErrorCode::Unauthorized, message)
264    }
265
266    pub fn forbidden(message: impl Into<String>) -> Self {
267        Self::new(ErrorCode::Forbidden, message)
268    }
269
270    pub fn internal_error(message: impl Into<String>) -> Self {
271        Self::new(ErrorCode::InternalError, message)
272    }
273
274    pub fn validation_error(message: impl Into<String>, errors: Vec<ValidationError>) -> Self {
275        Self::new(ErrorCode::ValidationError, message).with_validation_errors(errors)
276    }
277
278    pub fn conflict(message: impl Into<String>) -> Self {
279        Self::new(ErrorCode::ConflictError, message)
280    }
281}
282
283#[derive(Debug, Serialize, Deserialize)]
284pub struct ErrorResponse {
285    pub error: ApiError,
286
287    pub api_version: String,
288}
289
290#[cfg(feature = "web")]
291impl ErrorCode {
292    pub const fn status_code(&self) -> StatusCode {
293        match self {
294            Self::NotFound => StatusCode::NOT_FOUND,
295            Self::BadRequest => StatusCode::BAD_REQUEST,
296            Self::Unauthorized => StatusCode::UNAUTHORIZED,
297            Self::Forbidden => StatusCode::FORBIDDEN,
298            Self::ValidationError => StatusCode::UNPROCESSABLE_ENTITY,
299            Self::ConflictError => StatusCode::CONFLICT,
300            Self::RateLimited => StatusCode::TOO_MANY_REQUESTS,
301            Self::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
302            Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
303        }
304    }
305}
306
307#[cfg(feature = "web")]
308impl IntoResponse for ApiError {
309    fn into_response(self) -> axum::response::Response {
310        let status = self.code.status_code();
311
312        if status.is_server_error() {
313            tracing::error!(
314                error_code = ?self.code,
315                message = %self.message,
316                path = ?self.path,
317                trace_id = ?self.trace_id,
318                "API server error response"
319            );
320        } else if status.is_client_error() {
321            tracing::warn!(
322                error_code = ?self.code,
323                message = %self.message,
324                path = ?self.path,
325                trace_id = ?self.trace_id,
326                "API client error response"
327            );
328        }
329
330        (status, Json(self)).into_response()
331    }
332}
333
334#[cfg(feature = "web")]
335impl IntoResponse for InternalApiError {
336    fn into_response(self) -> axum::response::Response {
337        let error: ApiError = self.into();
338        error.into_response()
339    }
340}