1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use thiserror::Error;
9
10pub trait HttpError: std::error::Error + Send + Sync + 'static {
36 fn status_code(&self) -> u16 {
38 500
39 }
40
41 fn error_message(&self) -> String {
43 self.to_string()
44 }
45}
46
47#[derive(Debug, Clone)]
65pub struct AppError {
66 message: String,
67 status_code: u16,
68}
69
70impl AppError {
71 pub fn new(message: impl Into<String>) -> Self {
73 Self {
74 message: message.into(),
75 status_code: 500,
76 }
77 }
78
79 pub fn status(mut self, code: u16) -> Self {
81 self.status_code = code;
82 self
83 }
84
85 pub fn not_found(message: impl Into<String>) -> Self {
87 Self::new(message).status(404)
88 }
89
90 pub fn bad_request(message: impl Into<String>) -> Self {
92 Self::new(message).status(400)
93 }
94
95 pub fn unauthorized(message: impl Into<String>) -> Self {
97 Self::new(message).status(401)
98 }
99
100 pub fn forbidden(message: impl Into<String>) -> Self {
102 Self::new(message).status(403)
103 }
104
105 pub fn unprocessable(message: impl Into<String>) -> Self {
107 Self::new(message).status(422)
108 }
109
110 pub fn conflict(message: impl Into<String>) -> Self {
112 Self::new(message).status(409)
113 }
114}
115
116impl std::fmt::Display for AppError {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 write!(f, "{}", self.message)
119 }
120}
121
122impl std::error::Error for AppError {}
123
124impl HttpError for AppError {
125 fn status_code(&self) -> u16 {
126 self.status_code
127 }
128
129 fn error_message(&self) -> String {
130 self.message.clone()
131 }
132}
133
134impl From<AppError> for FrameworkError {
135 fn from(e: AppError) -> Self {
136 FrameworkError::Domain {
137 message: e.message,
138 status_code: e.status_code,
139 }
140 }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct ValidationErrors {
163 #[serde(flatten)]
165 pub errors: HashMap<String, Vec<String>>,
166}
167
168impl ValidationErrors {
169 pub fn new() -> Self {
171 Self {
172 errors: HashMap::new(),
173 }
174 }
175
176 pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
178 self.errors
179 .entry(field.into())
180 .or_default()
181 .push(message.into());
182 }
183
184 pub fn is_empty(&self) -> bool {
186 self.errors.is_empty()
187 }
188
189 pub fn from_validator(errors: validator::ValidationErrors) -> Self {
191 let mut result = Self::new();
192 for (field, field_errors) in errors.field_errors() {
193 for error in field_errors {
194 let message = error
195 .message
196 .as_ref()
197 .map(|m| m.to_string())
198 .unwrap_or_else(|| format!("Validation failed for field '{field}'"));
199 result.add(field.to_string(), message);
200 }
201 }
202 result
203 }
204
205 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#[derive(Debug, Clone, Error)]
260pub enum FrameworkError {
261 #[error("Service '{type_name}' not registered in container")]
263 ServiceNotFound {
264 type_name: &'static str,
266 },
267
268 #[error("Missing required parameter: {param_name}")]
270 ParamError {
271 param_name: String,
273 },
274
275 #[error("Validation error for '{field}': {message}")]
277 ValidationError {
278 field: String,
280 message: String,
282 },
283
284 #[error("Database error: {0}")]
286 Database(String),
287
288 #[error("Internal server error: {message}")]
290 Internal {
291 message: String,
293 },
294
295 #[error("{message}")]
299 Domain {
300 message: String,
302 status_code: u16,
304 },
305
306 #[error("Validation failed")]
310 Validation(ValidationErrors),
311
312 #[error("This action is unauthorized.")]
316 Unauthorized,
317
318 #[error("{model_name} not found")]
322 ModelNotFound {
323 model_name: String,
325 },
326
327 #[error("Invalid parameter '{param}': expected {expected_type}")]
331 ParamParse {
332 param: String,
334 expected_type: &'static str,
336 },
337}
338
339impl FrameworkError {
340 pub fn service_not_found<T: ?Sized>() -> Self {
342 Self::ServiceNotFound {
343 type_name: std::any::type_name::<T>(),
344 }
345 }
346
347 pub fn param(name: impl Into<String>) -> Self {
349 Self::ParamError {
350 param_name: name.into(),
351 }
352 }
353
354 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 pub fn database(message: impl Into<String>) -> Self {
364 Self::Database(message.into())
365 }
366
367 pub fn internal(message: impl Into<String>) -> Self {
369 Self::Internal {
370 message: message.into(),
371 }
372 }
373
374 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 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 pub fn validation_errors(errors: ValidationErrors) -> Self {
400 Self::Validation(errors)
401 }
402
403 pub fn model_not_found(name: impl Into<String>) -> Self {
405 Self::ModelNotFound {
406 model_name: name.into(),
407 }
408 }
409
410 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 pub fn hint(&self) -> Option<String> {
425 match self {
426 Self::ServiceNotFound { type_name } => Some(format!(
427 "Register with App::bind::<{type_name}>() in your bootstrap.rs or a service provider"
428 )),
429 Self::ParamError { param_name } => Some(format!(
430 "Check your route definition includes :{param_name} or verify the request body contains this field"
431 )),
432 Self::ModelNotFound { model_name } => Some(format!(
433 "Verify the {model_name} exists in the database, or check that the route parameter matches a valid ID"
434 )),
435 Self::ParamParse {
436 param,
437 expected_type,
438 } => Some(format!(
439 "Route received '{param}' but expected a valid {expected_type}. Check the URL parameter format."
440 )),
441 Self::Database(_) => Some(
442 "Check DATABASE_URL in .env and verify the database is running".to_string(),
443 ),
444 Self::Unauthorized => Some(
445 "Check that the handler's form request authorize() returns true, or verify the user has the required permissions".to_string(),
446 ),
447 Self::Internal { .. } | Self::Domain { .. } | Self::ValidationError { .. } | Self::Validation(_) => None,
449 }
450 }
451}
452
453impl From<sea_orm::DbErr> for FrameworkError {
455 fn from(e: sea_orm::DbErr) -> Self {
456 Self::Database(e.to_string())
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463 use crate::http::HttpResponse;
464
465 fn error_to_json(err: FrameworkError) -> serde_json::Value {
467 let resp: HttpResponse = err.into();
468 serde_json::from_str(resp.body()).expect("response body should be valid JSON")
469 }
470
471 #[test]
472 fn service_not_found_includes_hint() {
473 let err = FrameworkError::service_not_found::<String>();
474 let json = error_to_json(err);
475
476 assert!(json.get("message").is_some(), "should have 'message' key");
477 let hint = json
478 .get("hint")
479 .and_then(|v| v.as_str())
480 .expect("should have 'hint' key");
481 assert!(
482 hint.contains("App::bind"),
483 "hint should mention App::bind, got: {hint}"
484 );
485 }
486
487 #[test]
488 fn param_error_includes_hint() {
489 let err = FrameworkError::param("user_id");
490 let json = error_to_json(err);
491
492 assert!(json.get("message").is_some());
493 let hint = json
494 .get("hint")
495 .and_then(|v| v.as_str())
496 .expect("should have hint");
497 assert!(
498 hint.contains(":user_id"),
499 "hint should reference param name, got: {hint}"
500 );
501 }
502
503 #[test]
504 fn model_not_found_includes_hint() {
505 let err = FrameworkError::model_not_found("User");
506 let json = error_to_json(err);
507
508 assert_eq!(json["message"], "User not found");
509 let hint = json
510 .get("hint")
511 .and_then(|v| v.as_str())
512 .expect("should have hint");
513 assert!(hint.contains("User"), "hint should reference model name");
514 }
515
516 #[test]
517 fn param_parse_includes_hint() {
518 let err = FrameworkError::param_parse("abc", "i32");
519 let json = error_to_json(err);
520
521 let hint = json
522 .get("hint")
523 .and_then(|v| v.as_str())
524 .expect("should have hint");
525 assert!(hint.contains("abc"), "hint should include received value");
526 assert!(hint.contains("i32"), "hint should include expected type");
527 }
528
529 #[test]
530 fn database_error_includes_hint() {
531 let err = FrameworkError::database("connection refused");
532 let json = error_to_json(err);
533
534 let hint = json
535 .get("hint")
536 .and_then(|v| v.as_str())
537 .expect("should have hint");
538 assert!(
539 hint.contains("DATABASE_URL"),
540 "hint should mention DATABASE_URL"
541 );
542 }
543
544 #[test]
545 fn unauthorized_includes_hint() {
546 let err = FrameworkError::Unauthorized;
547 let json = error_to_json(err);
548
549 assert_eq!(json["message"], "This action is unauthorized.");
550 let hint = json
551 .get("hint")
552 .and_then(|v| v.as_str())
553 .expect("should have hint");
554 assert!(
555 hint.contains("authorize()"),
556 "hint should mention authorize()"
557 );
558 }
559
560 #[test]
561 fn internal_error_has_no_hint() {
562 let err = FrameworkError::internal("something broke");
563 let json = error_to_json(err);
564
565 assert!(json.get("message").is_some());
566 assert!(
567 json.get("hint").is_none(),
568 "Internal errors should not have hints"
569 );
570 }
571
572 #[test]
573 fn domain_error_has_no_hint() {
574 let err = FrameworkError::domain("custom message", 409);
575 let json = error_to_json(err);
576
577 assert_eq!(json["message"], "custom message");
578 assert!(
579 json.get("hint").is_none(),
580 "Domain errors should not have hints"
581 );
582 }
583
584 #[test]
585 fn validation_errors_have_no_hint() {
586 let mut errors = ValidationErrors::new();
587 errors.add("email", "Email is required");
588 let err = FrameworkError::validation_errors(errors);
589 let json = error_to_json(err);
590
591 assert!(
592 json.get("hint").is_none(),
593 "Validation errors should not have hints"
594 );
595 assert!(json.get("errors").is_some(), "should have errors field");
596 }
597
598 #[test]
599 fn status_codes_are_correct() {
600 assert_eq!(
601 FrameworkError::service_not_found::<String>().status_code(),
602 500
603 );
604 assert_eq!(FrameworkError::param("x").status_code(), 400);
605 assert_eq!(FrameworkError::model_not_found("X").status_code(), 404);
606 assert_eq!(FrameworkError::param_parse("x", "i32").status_code(), 400);
607 assert_eq!(FrameworkError::database("err").status_code(), 500);
608 assert_eq!(FrameworkError::internal("err").status_code(), 500);
609 assert_eq!(FrameworkError::domain("err", 409).status_code(), 409);
610 assert_eq!(FrameworkError::Unauthorized.status_code(), 403);
611 }
612}