kit_rs/
error.rs

1//! Framework-wide error types
2//!
3//! Provides a unified error type that can be used throughout the framework
4//! and automatically converts to appropriate HTTP responses.
5
6use std::collections::HashMap;
7use thiserror::Error;
8
9/// Trait for errors that can be converted to HTTP responses
10///
11/// Implement this trait on your domain errors to customize the HTTP status code
12/// and message that will be returned when the error is converted to a response.
13///
14/// # Example
15///
16/// ```rust,ignore
17/// use kit::HttpError;
18///
19/// #[derive(Debug)]
20/// struct UserNotFoundError { user_id: i32 }
21///
22/// impl std::fmt::Display for UserNotFoundError {
23///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24///         write!(f, "User {} not found", self.user_id)
25///     }
26/// }
27///
28/// impl std::error::Error for UserNotFoundError {}
29///
30/// impl HttpError for UserNotFoundError {
31///     fn status_code(&self) -> u16 { 404 }
32/// }
33/// ```
34pub trait HttpError: std::error::Error + Send + Sync + 'static {
35    /// HTTP status code (default: 500)
36    fn status_code(&self) -> u16 {
37        500
38    }
39
40    /// Error message for HTTP response (default: error's Display)
41    fn error_message(&self) -> String {
42        self.to_string()
43    }
44}
45
46/// Simple wrapper for creating one-off domain errors
47///
48/// Use this for inline/ad-hoc errors when you don't want to create
49/// a dedicated error type.
50///
51/// # Example
52///
53/// ```rust,ignore
54/// use kit::{AppError, FrameworkError};
55///
56/// pub async fn process() -> Result<(), FrameworkError> {
57///     if invalid {
58///         return Err(AppError::bad_request("Invalid input").into());
59///     }
60///     Ok(())
61/// }
62/// ```
63#[derive(Debug, Clone)]
64pub struct AppError {
65    message: String,
66    status_code: u16,
67}
68
69impl AppError {
70    /// Create a new AppError with status 500 (Internal Server Error)
71    pub fn new(message: impl Into<String>) -> Self {
72        Self {
73            message: message.into(),
74            status_code: 500,
75        }
76    }
77
78    /// Set the HTTP status code
79    pub fn status(mut self, code: u16) -> Self {
80        self.status_code = code;
81        self
82    }
83
84    /// Create a 404 Not Found error
85    pub fn not_found(message: impl Into<String>) -> Self {
86        Self::new(message).status(404)
87    }
88
89    /// Create a 400 Bad Request error
90    pub fn bad_request(message: impl Into<String>) -> Self {
91        Self::new(message).status(400)
92    }
93
94    /// Create a 401 Unauthorized error
95    pub fn unauthorized(message: impl Into<String>) -> Self {
96        Self::new(message).status(401)
97    }
98
99    /// Create a 403 Forbidden error
100    pub fn forbidden(message: impl Into<String>) -> Self {
101        Self::new(message).status(403)
102    }
103
104    /// Create a 422 Unprocessable Entity error
105    pub fn unprocessable(message: impl Into<String>) -> Self {
106        Self::new(message).status(422)
107    }
108
109    /// Create a 409 Conflict error
110    pub fn conflict(message: impl Into<String>) -> Self {
111        Self::new(message).status(409)
112    }
113}
114
115impl std::fmt::Display for AppError {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        write!(f, "{}", self.message)
118    }
119}
120
121impl std::error::Error for AppError {}
122
123impl HttpError for AppError {
124    fn status_code(&self) -> u16 {
125        self.status_code
126    }
127
128    fn error_message(&self) -> String {
129        self.message.clone()
130    }
131}
132
133impl From<AppError> for FrameworkError {
134    fn from(e: AppError) -> Self {
135        FrameworkError::Domain {
136            message: e.message,
137            status_code: e.status_code,
138        }
139    }
140}
141
142/// Validation errors with Laravel/Inertia-compatible format
143///
144/// Contains a map of field names to error messages, supporting multiple
145/// errors per field.
146///
147/// # Response Format
148///
149/// When converted to an HTTP response, produces Laravel-compatible JSON:
150///
151/// ```json
152/// {
153///     "message": "The given data was invalid.",
154///     "errors": {
155///         "email": ["The email field must be a valid email address."],
156///         "password": ["The password field must be at least 8 characters."]
157///     }
158/// }
159/// ```
160#[derive(Debug, Clone)]
161pub struct ValidationErrors {
162    /// Map of field names to their validation error messages
163    pub errors: HashMap<String, Vec<String>>,
164}
165
166impl ValidationErrors {
167    /// Create a new empty ValidationErrors
168    pub fn new() -> Self {
169        Self {
170            errors: HashMap::new(),
171        }
172    }
173
174    /// Add an error for a specific field
175    pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
176        self.errors
177            .entry(field.into())
178            .or_default()
179            .push(message.into());
180    }
181
182    /// Check if there are any errors
183    pub fn is_empty(&self) -> bool {
184        self.errors.is_empty()
185    }
186
187    /// Convert from validator crate's ValidationErrors
188    pub fn from_validator(errors: validator::ValidationErrors) -> Self {
189        let mut result = Self::new();
190        for (field, field_errors) in errors.field_errors() {
191            for error in field_errors {
192                let message = error
193                    .message
194                    .as_ref()
195                    .map(|m| m.to_string())
196                    .unwrap_or_else(|| format!("Validation failed for field '{}'", field));
197                result.add(field.to_string(), message);
198            }
199        }
200        result
201    }
202
203    /// Convert to JSON Value for response
204    pub fn to_json(&self) -> serde_json::Value {
205        serde_json::json!({
206            "message": "The given data was invalid.",
207            "errors": self.errors
208        })
209    }
210}
211
212impl Default for ValidationErrors {
213    fn default() -> Self {
214        Self::new()
215    }
216}
217
218impl std::fmt::Display for ValidationErrors {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        write!(f, "Validation failed: {:?}", self.errors)
221    }
222}
223
224impl std::error::Error for ValidationErrors {}
225
226/// Framework-wide error type
227///
228/// This enum represents all possible errors that can occur in the framework.
229/// It implements `From<FrameworkError> for Response` so errors can be propagated
230/// using the `?` operator in controller handlers.
231///
232/// # Example
233///
234/// ```rust,ignore
235/// use kit::{App, FrameworkError, Response};
236///
237/// pub async fn index(_req: Request) -> Response {
238///     let service = App::resolve::<MyService>()?;  // Returns FrameworkError on failure
239///     // ...
240/// }
241/// ```
242///
243/// # Automatic Error Conversion
244///
245/// `FrameworkError` implements `From` for common error types, allowing seamless
246/// use of the `?` operator:
247///
248/// ```rust,ignore
249/// use kit::{DB, FrameworkError};
250/// use sea_orm::ActiveModelTrait;
251///
252/// pub async fn create_todo() -> Result<Todo, FrameworkError> {
253///     let todo = new_todo.insert(&*DB::get()?).await?;  // DbErr converts automatically!
254///     Ok(todo)
255/// }
256/// ```
257#[derive(Debug, Clone, Error)]
258pub enum FrameworkError {
259    /// Service not found in the dependency injection container
260    #[error("Service '{type_name}' not registered in container")]
261    ServiceNotFound {
262        /// The type name of the service that was not found
263        type_name: &'static str,
264    },
265
266    /// Parameter extraction failed (missing or invalid parameter)
267    #[error("Missing required parameter: {param_name}")]
268    ParamError {
269        /// The name of the parameter that failed extraction
270        param_name: String,
271    },
272
273    /// Validation error
274    #[error("Validation error for '{field}': {message}")]
275    ValidationError {
276        /// The field that failed validation
277        field: String,
278        /// The validation error message
279        message: String,
280    },
281
282    /// Database error
283    #[error("Database error: {0}")]
284    Database(String),
285
286    /// Generic internal server error
287    #[error("Internal server error: {message}")]
288    Internal {
289        /// The error message
290        message: String,
291    },
292
293    /// Domain/application error with custom status code
294    ///
295    /// Used for user-defined domain errors that need custom HTTP status codes.
296    #[error("{message}")]
297    Domain {
298        /// The error message
299        message: String,
300        /// HTTP status code
301        status_code: u16,
302    },
303
304    /// Form validation errors (422 Unprocessable Entity)
305    ///
306    /// Contains multiple field validation errors in Laravel/Inertia format.
307    #[error("Validation failed")]
308    Validation(ValidationErrors),
309
310    /// Authorization failed (403 Forbidden)
311    ///
312    /// Used when FormRequest::authorize() returns false.
313    #[error("This action is unauthorized.")]
314    Unauthorized,
315
316    /// Model not found (404 Not Found)
317    ///
318    /// Used when route model binding fails to find the requested resource.
319    #[error("{model_name} not found")]
320    ModelNotFound {
321        /// The name of the model that was not found
322        model_name: String,
323    },
324
325    /// Parameter parse error (400 Bad Request)
326    ///
327    /// Used when a path parameter cannot be parsed to the expected type.
328    #[error("Invalid parameter '{param}': expected {expected_type}")]
329    ParamParse {
330        /// The parameter value that failed to parse
331        param: String,
332        /// The expected type (e.g., "i32", "uuid")
333        expected_type: &'static str,
334    },
335}
336
337impl FrameworkError {
338    /// Create a ServiceNotFound error for a given type
339    pub fn service_not_found<T: ?Sized>() -> Self {
340        Self::ServiceNotFound {
341            type_name: std::any::type_name::<T>(),
342        }
343    }
344
345    /// Create a ParamError for a missing parameter
346    pub fn param(name: impl Into<String>) -> Self {
347        Self::ParamError {
348            param_name: name.into(),
349        }
350    }
351
352    /// Create a ValidationError
353    pub fn validation(field: impl Into<String>, message: impl Into<String>) -> Self {
354        Self::ValidationError {
355            field: field.into(),
356            message: message.into(),
357        }
358    }
359
360    /// Create a DatabaseError
361    pub fn database(message: impl Into<String>) -> Self {
362        Self::Database(message.into())
363    }
364
365    /// Create an Internal error
366    pub fn internal(message: impl Into<String>) -> Self {
367        Self::Internal {
368            message: message.into(),
369        }
370    }
371
372    /// Create a Domain error with custom status code
373    pub fn domain(message: impl Into<String>, status_code: u16) -> Self {
374        Self::Domain {
375            message: message.into(),
376            status_code,
377        }
378    }
379
380    /// Get the HTTP status code for this error
381    pub fn status_code(&self) -> u16 {
382        match self {
383            Self::ServiceNotFound { .. } => 500,
384            Self::ParamError { .. } => 400,
385            Self::ValidationError { .. } => 422,
386            Self::Database(_) => 500,
387            Self::Internal { .. } => 500,
388            Self::Domain { status_code, .. } => *status_code,
389            Self::Validation(_) => 422,
390            Self::Unauthorized => 403,
391            Self::ModelNotFound { .. } => 404,
392            Self::ParamParse { .. } => 400,
393        }
394    }
395
396    /// Create a Validation error from ValidationErrors struct
397    pub fn validation_errors(errors: ValidationErrors) -> Self {
398        Self::Validation(errors)
399    }
400
401    /// Create a ModelNotFound error (404)
402    pub fn model_not_found(name: impl Into<String>) -> Self {
403        Self::ModelNotFound {
404            model_name: name.into(),
405        }
406    }
407
408    /// Create a ParamParse error (400)
409    pub fn param_parse(param: impl Into<String>, expected_type: &'static str) -> Self {
410        Self::ParamParse {
411            param: param.into(),
412            expected_type,
413        }
414    }
415}
416
417// Implement From<DbErr> for automatic error conversion with ?
418impl From<sea_orm::DbErr> for FrameworkError {
419    fn from(e: sea_orm::DbErr) -> Self {
420        Self::Database(e.to_string())
421    }
422}