rustapi_core/
error.rs

1//! Error types for RustAPI
2
3use http::StatusCode;
4use serde::Serialize;
5use std::fmt;
6
7/// Result type alias for RustAPI operations
8pub type Result<T, E = ApiError> = std::result::Result<T, E>;
9
10/// Standard API error type
11///
12/// Provides structured error responses following a consistent JSON format.
13#[derive(Debug, Clone)]
14pub struct ApiError {
15    /// HTTP status code
16    pub status: StatusCode,
17    /// Error type identifier
18    pub error_type: String,
19    /// Human-readable error message
20    pub message: String,
21    /// Optional field-level validation errors
22    pub fields: Option<Vec<FieldError>>,
23    /// Internal details (hidden in production)
24    pub(crate) internal: Option<String>,
25}
26
27/// Field-level validation error
28#[derive(Debug, Clone, Serialize)]
29pub struct FieldError {
30    /// Field name (supports nested: "address.city")
31    pub field: String,
32    /// Error code (e.g., "email", "length", "required")
33    pub code: String,
34    /// Human-readable message
35    pub message: String,
36}
37
38impl ApiError {
39    /// Create a new API error
40    pub fn new(status: StatusCode, error_type: impl Into<String>, message: impl Into<String>) -> Self {
41        Self {
42            status,
43            error_type: error_type.into(),
44            message: message.into(),
45            fields: None,
46            internal: None,
47        }
48    }
49
50    /// Create a validation error with field details
51    pub fn validation(fields: Vec<FieldError>) -> Self {
52        Self {
53            status: StatusCode::UNPROCESSABLE_ENTITY,
54            error_type: "validation_error".to_string(),
55            message: "Request validation failed".to_string(),
56            fields: Some(fields),
57            internal: None,
58        }
59    }
60
61    /// Create a 400 Bad Request error
62    pub fn bad_request(message: impl Into<String>) -> Self {
63        Self::new(StatusCode::BAD_REQUEST, "bad_request", message)
64    }
65
66    /// Create a 401 Unauthorized error
67    pub fn unauthorized(message: impl Into<String>) -> Self {
68        Self::new(StatusCode::UNAUTHORIZED, "unauthorized", message)
69    }
70
71    /// Create a 403 Forbidden error
72    pub fn forbidden(message: impl Into<String>) -> Self {
73        Self::new(StatusCode::FORBIDDEN, "forbidden", message)
74    }
75
76    /// Create a 404 Not Found error
77    pub fn not_found(message: impl Into<String>) -> Self {
78        Self::new(StatusCode::NOT_FOUND, "not_found", message)
79    }
80
81    /// Create a 409 Conflict error
82    pub fn conflict(message: impl Into<String>) -> Self {
83        Self::new(StatusCode::CONFLICT, "conflict", message)
84    }
85
86    /// Create a 500 Internal Server Error
87    pub fn internal(message: impl Into<String>) -> Self {
88        Self::new(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", message)
89    }
90
91    /// Add internal details (for logging, hidden from response in prod)
92    pub fn with_internal(mut self, details: impl Into<String>) -> Self {
93        self.internal = Some(details.into());
94        self
95    }
96}
97
98impl fmt::Display for ApiError {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(f, "{}: {}", self.error_type, self.message)
101    }
102}
103
104impl std::error::Error for ApiError {}
105
106/// JSON representation of API error response
107#[derive(Serialize)]
108pub(crate) struct ErrorResponse {
109    pub error: ErrorBody,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub request_id: Option<String>,
112}
113
114#[derive(Serialize)]
115pub(crate) struct ErrorBody {
116    #[serde(rename = "type")]
117    pub error_type: String,
118    pub message: String,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub fields: Option<Vec<FieldError>>,
121}
122
123impl From<ApiError> for ErrorResponse {
124    fn from(err: ApiError) -> Self {
125        Self {
126            error: ErrorBody {
127                error_type: err.error_type,
128                message: err.message,
129                fields: err.fields,
130            },
131            request_id: None, // TODO: inject from request context
132        }
133    }
134}
135
136// Conversion from common error types
137impl From<serde_json::Error> for ApiError {
138    fn from(err: serde_json::Error) -> Self {
139        ApiError::bad_request(format!("Invalid JSON: {}", err))
140    }
141}
142
143impl From<std::io::Error> for ApiError {
144    fn from(err: std::io::Error) -> Self {
145        ApiError::internal("I/O error").with_internal(err.to_string())
146    }
147}
148
149impl From<hyper::Error> for ApiError {
150    fn from(err: hyper::Error) -> Self {
151        ApiError::internal("HTTP error").with_internal(err.to_string())
152    }
153}
154
155impl From<rustapi_validate::ValidationError> for ApiError {
156    fn from(err: rustapi_validate::ValidationError) -> Self {
157        let fields = err.fields.into_iter().map(|f| FieldError {
158            field: f.field,
159            code: f.code,
160            message: f.message,
161        }).collect();
162        
163        ApiError::validation(fields)
164    }
165}
166
167impl ApiError {
168    /// Create a validation error from a ValidationError
169    pub fn from_validation_error(err: rustapi_validate::ValidationError) -> Self {
170        err.into()
171    }
172}
173