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(|| {
197 format!("Validation failed for field '{}'", field)
198 });
199 result.add(field.to_string(), message);
200 }
201 }
202 result
203 }
204
205 /// Convert to JSON Value for response
206 pub fn to_json(&self) -> serde_json::Value {
207 serde_json::json!({
208 "message": "The given data was invalid.",
209 "errors": self.errors
210 })
211 }
212}
213
214impl Default for ValidationErrors {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220impl std::fmt::Display for ValidationErrors {
221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222 write!(f, "Validation failed: {:?}", self.errors)
223 }
224}
225
226impl std::error::Error for ValidationErrors {}
227
228/// Framework-wide error type
229///
230/// This enum represents all possible errors that can occur in the framework.
231/// It implements `From<FrameworkError> for Response` so errors can be propagated
232/// using the `?` operator in controller handlers.
233///
234/// # Example
235///
236/// ```rust,ignore
237/// use kit::{App, FrameworkError, Response};
238///
239/// pub async fn index(_req: Request) -> Response {
240/// let service = App::resolve::<MyService>()?; // Returns FrameworkError on failure
241/// // ...
242/// }
243/// ```
244///
245/// # Automatic Error Conversion
246///
247/// `FrameworkError` implements `From` for common error types, allowing seamless
248/// use of the `?` operator:
249///
250/// ```rust,ignore
251/// use kit::{DB, FrameworkError};
252/// use sea_orm::ActiveModelTrait;
253///
254/// pub async fn create_todo() -> Result<Todo, FrameworkError> {
255/// let todo = new_todo.insert(&*DB::get()?).await?; // DbErr converts automatically!
256/// Ok(todo)
257/// }
258/// ```
259#[derive(Debug, Clone, Error)]
260pub enum FrameworkError {
261 /// Service not found in the dependency injection container
262 #[error("Service '{type_name}' not registered in container")]
263 ServiceNotFound {
264 /// The type name of the service that was not found
265 type_name: &'static str,
266 },
267
268 /// Parameter extraction failed (missing or invalid parameter)
269 #[error("Missing required parameter: {param_name}")]
270 ParamError {
271 /// The name of the parameter that failed extraction
272 param_name: String,
273 },
274
275 /// Validation error
276 #[error("Validation error for '{field}': {message}")]
277 ValidationError {
278 /// The field that failed validation
279 field: String,
280 /// The validation error message
281 message: String,
282 },
283
284 /// Database error
285 #[error("Database error: {0}")]
286 Database(String),
287
288 /// Generic internal server error
289 #[error("Internal server error: {message}")]
290 Internal {
291 /// The error message
292 message: String,
293 },
294
295 /// Domain/application error with custom status code
296 ///
297 /// Used for user-defined domain errors that need custom HTTP status codes.
298 #[error("{message}")]
299 Domain {
300 /// The error message
301 message: String,
302 /// HTTP status code
303 status_code: u16,
304 },
305
306 /// Form validation errors (422 Unprocessable Entity)
307 ///
308 /// Contains multiple field validation errors in Laravel/Inertia format.
309 #[error("Validation failed")]
310 Validation(ValidationErrors),
311
312 /// Authorization failed (403 Forbidden)
313 ///
314 /// Used when FormRequest::authorize() returns false.
315 #[error("This action is unauthorized.")]
316 Unauthorized,
317
318 /// Model not found (404 Not Found)
319 ///
320 /// Used when route model binding fails to find the requested resource.
321 #[error("{model_name} not found")]
322 ModelNotFound {
323 /// The name of the model that was not found
324 model_name: String,
325 },
326
327 /// Parameter parse error (400 Bad Request)
328 ///
329 /// Used when a path parameter cannot be parsed to the expected type.
330 #[error("Invalid parameter '{param}': expected {expected_type}")]
331 ParamParse {
332 /// The parameter value that failed to parse
333 param: String,
334 /// The expected type (e.g., "i32", "uuid")
335 expected_type: &'static str,
336 },
337}
338
339impl FrameworkError {
340 /// Create a ServiceNotFound error for a given type
341 pub fn service_not_found<T: ?Sized>() -> Self {
342 Self::ServiceNotFound {
343 type_name: std::any::type_name::<T>(),
344 }
345 }
346
347 /// Create a ParamError for a missing parameter
348 pub fn param(name: impl Into<String>) -> Self {
349 Self::ParamError {
350 param_name: name.into(),
351 }
352 }
353
354 /// Create a ValidationError
355 pub fn validation(field: impl Into<String>, message: impl Into<String>) -> Self {
356 Self::ValidationError {
357 field: field.into(),
358 message: message.into(),
359 }
360 }
361
362 /// Create a DatabaseError
363 pub fn database(message: impl Into<String>) -> Self {
364 Self::Database(message.into())
365 }
366
367 /// Create an Internal error
368 pub fn internal(message: impl Into<String>) -> Self {
369 Self::Internal {
370 message: message.into(),
371 }
372 }
373
374 /// Create a Domain error with custom status code
375 pub fn domain(message: impl Into<String>, status_code: u16) -> Self {
376 Self::Domain {
377 message: message.into(),
378 status_code,
379 }
380 }
381
382 /// Get the HTTP status code for this error
383 pub fn status_code(&self) -> u16 {
384 match self {
385 Self::ServiceNotFound { .. } => 500,
386 Self::ParamError { .. } => 400,
387 Self::ValidationError { .. } => 422,
388 Self::Database(_) => 500,
389 Self::Internal { .. } => 500,
390 Self::Domain { status_code, .. } => *status_code,
391 Self::Validation(_) => 422,
392 Self::Unauthorized => 403,
393 Self::ModelNotFound { .. } => 404,
394 Self::ParamParse { .. } => 400,
395 }
396 }
397
398 /// Create a Validation error from ValidationErrors struct
399 pub fn validation_errors(errors: ValidationErrors) -> Self {
400 Self::Validation(errors)
401 }
402
403 /// Create a ModelNotFound error (404)
404 pub fn model_not_found(name: impl Into<String>) -> Self {
405 Self::ModelNotFound {
406 model_name: name.into(),
407 }
408 }
409
410 /// Create a ParamParse error (400)
411 pub fn param_parse(param: impl Into<String>, expected_type: &'static str) -> Self {
412 Self::ParamParse {
413 param: param.into(),
414 expected_type,
415 }
416 }
417}
418
419// Implement From<DbErr> for automatic error conversion with ?
420impl From<sea_orm::DbErr> for FrameworkError {
421 fn from(e: sea_orm::DbErr) -> Self {
422 Self::Database(e.to_string())
423 }
424}