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