Skip to main content

systemprompt_models/api/errors/
internal.rs

1//! Internal `thiserror`-derived error type used by the application
2//! tier and converted into the public [`super::ApiError`] envelope at
3//! the HTTP boundary.
4
5use super::{ApiError, ErrorCode};
6
7#[derive(Debug, thiserror::Error)]
8pub enum InternalApiError {
9    #[error("Resource not found: {resource_type} with ID '{id}'")]
10    NotFound { resource_type: String, id: String },
11
12    #[error("Bad request: {message}")]
13    BadRequest { message: String },
14
15    #[error("Unauthorized access: {reason}")]
16    Unauthorized { reason: String },
17
18    #[error("Access forbidden: {resource} - {reason}")]
19    Forbidden { resource: String, reason: String },
20
21    #[error("Validation failed for field '{field}': {reason}")]
22    ValidationError { field: String, reason: String },
23
24    #[error("Conflict: {resource} already exists")]
25    ConflictError { resource: String },
26
27    #[error("Rate limit exceeded for {resource}")]
28    RateLimited { resource: String },
29
30    #[error("Service temporarily unavailable: {service}")]
31    ServiceUnavailable { service: String },
32
33    #[error("Database operation failed: {message}")]
34    DatabaseError { message: String },
35
36    #[error("JSON serialization failed")]
37    JsonError(#[from] serde_json::Error),
38
39    #[error("Authentication token error: {message}")]
40    AuthenticationError { message: String },
41
42    #[error("Internal server error: {message}")]
43    InternalError { message: String },
44}
45
46impl InternalApiError {
47    pub fn not_found(resource_type: impl Into<String>, id: impl Into<String>) -> Self {
48        Self::NotFound {
49            resource_type: resource_type.into(),
50            id: id.into(),
51        }
52    }
53
54    pub fn bad_request(message: impl Into<String>) -> Self {
55        Self::BadRequest {
56            message: message.into(),
57        }
58    }
59
60    pub fn unauthorized(reason: impl Into<String>) -> Self {
61        Self::Unauthorized {
62            reason: reason.into(),
63        }
64    }
65
66    pub fn forbidden(resource: impl Into<String>, reason: impl Into<String>) -> Self {
67        Self::Forbidden {
68            resource: resource.into(),
69            reason: reason.into(),
70        }
71    }
72
73    pub fn validation_error(field: impl Into<String>, reason: impl Into<String>) -> Self {
74        Self::ValidationError {
75            field: field.into(),
76            reason: reason.into(),
77        }
78    }
79
80    pub fn conflict(resource: impl Into<String>) -> Self {
81        Self::ConflictError {
82            resource: resource.into(),
83        }
84    }
85
86    pub fn rate_limited(resource: impl Into<String>) -> Self {
87        Self::RateLimited {
88            resource: resource.into(),
89        }
90    }
91
92    pub fn service_unavailable(service: impl Into<String>) -> Self {
93        Self::ServiceUnavailable {
94            service: service.into(),
95        }
96    }
97
98    pub fn internal_error(message: impl Into<String>) -> Self {
99        Self::InternalError {
100            message: message.into(),
101        }
102    }
103
104    pub fn database_error(message: impl Into<String>) -> Self {
105        Self::DatabaseError {
106            message: message.into(),
107        }
108    }
109
110    pub fn authentication_error(message: impl Into<String>) -> Self {
111        Self::AuthenticationError {
112            message: message.into(),
113        }
114    }
115
116    #[must_use]
117    pub const fn error_code(&self) -> ErrorCode {
118        match self {
119            Self::NotFound { .. } => ErrorCode::NotFound,
120            Self::BadRequest { .. } => ErrorCode::BadRequest,
121            Self::Unauthorized { .. } => ErrorCode::Unauthorized,
122            Self::Forbidden { .. } => ErrorCode::Forbidden,
123            Self::ValidationError { .. } => ErrorCode::ValidationError,
124            Self::ConflictError { .. } => ErrorCode::ConflictError,
125            Self::RateLimited { .. } => ErrorCode::RateLimited,
126            Self::ServiceUnavailable { .. } => ErrorCode::ServiceUnavailable,
127            Self::DatabaseError { .. }
128            | Self::JsonError(_)
129            | Self::AuthenticationError { .. }
130            | Self::InternalError { .. } => ErrorCode::InternalError,
131        }
132    }
133}
134
135impl From<InternalApiError> for ApiError {
136    fn from(error: InternalApiError) -> Self {
137        let code = error.error_code();
138        let message = error.to_string();
139        let details = match &error {
140            InternalApiError::NotFound { resource_type, id } => Some(format!(
141                "The requested {resource_type} with ID '{id}' does not exist"
142            )),
143            InternalApiError::ValidationError { field, reason } => {
144                Some(format!("Field '{field}': {reason}"))
145            },
146            InternalApiError::Forbidden { resource, reason } => {
147                Some(format!("Access to {resource} denied: {reason}"))
148            },
149            InternalApiError::DatabaseError { message } => {
150                Some(format!("Database error: {message}"))
151            },
152            InternalApiError::JsonError(e) => Some(format!("JSON processing error: {e}")),
153            InternalApiError::AuthenticationError { message } => {
154                Some(format!("Authentication error: {message}"))
155            },
156            InternalApiError::BadRequest { .. }
157            | InternalApiError::Unauthorized { .. }
158            | InternalApiError::ConflictError { .. }
159            | InternalApiError::RateLimited { .. }
160            | InternalApiError::ServiceUnavailable { .. }
161            | InternalApiError::InternalError { .. } => None,
162        };
163
164        let api_error = Self::new(code, message);
165        if let Some(d) = details {
166            api_error.with_details(d)
167        } else {
168            api_error
169        }
170    }
171}
172
173#[cfg(feature = "web")]
174impl axum::response::IntoResponse for InternalApiError {
175    fn into_response(self) -> axum::response::Response {
176        let error: ApiError = self.into();
177        error.into_response()
178    }
179}