Skip to main content

anvil_core/
error.rs

1//! Unified error type for Anvil. Implements `IntoResponse` so handlers `?`-propagate freely.
2
3use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use axum::Json;
6use serde_json::json;
7use thiserror::Error;
8
9pub type Result<T, E = Error> = std::result::Result<T, E>;
10
11#[derive(Debug, Error)]
12pub enum Error {
13    #[error("not found")]
14    NotFound,
15
16    #[error("unauthenticated")]
17    Unauthenticated,
18
19    #[error("forbidden: {0}")]
20    Forbidden(String),
21
22    #[error("validation failed")]
23    Validation(ValidationErrors),
24
25    #[error("bad request: {0}")]
26    BadRequest(String),
27
28    #[error("conflict: {0}")]
29    Conflict(String),
30
31    #[error("database error: {0}")]
32    Database(#[from] sqlx::Error),
33
34    #[error("io error: {0}")]
35    Io(#[from] std::io::Error),
36
37    #[error("serialization error: {0}")]
38    Serialization(#[from] serde_json::Error),
39
40    #[error("config error: {0}")]
41    Config(String),
42
43    #[error("template error: {0}")]
44    Template(String),
45
46    #[error("queue error: {0}")]
47    Queue(String),
48
49    #[error("mail error: {0}")]
50    Mail(String),
51
52    #[error("cache error: {0}")]
53    Cache(String),
54
55    #[error("storage error: {0}")]
56    Storage(String),
57
58    #[error("internal server error: {0}")]
59    Internal(String),
60
61    #[error("{0}")]
62    Other(#[from] anyhow::Error),
63}
64
65#[derive(Debug, Clone, Default, serde::Serialize)]
66pub struct ValidationErrors {
67    pub errors: indexmap::IndexMap<String, Vec<String>>,
68}
69
70impl ValidationErrors {
71    pub fn new() -> Self {
72        Self {
73            errors: indexmap::IndexMap::new(),
74        }
75    }
76
77    pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
78        self.errors
79            .entry(field.into())
80            .or_default()
81            .push(message.into());
82    }
83
84    pub fn is_empty(&self) -> bool {
85        self.errors.is_empty()
86    }
87}
88
89impl std::fmt::Display for ValidationErrors {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        for (field, msgs) in &self.errors {
92            for msg in msgs {
93                writeln!(f, "{field}: {msg}")?;
94            }
95        }
96        Ok(())
97    }
98}
99
100impl Error {
101    pub fn status(&self) -> StatusCode {
102        match self {
103            Error::NotFound => StatusCode::NOT_FOUND,
104            Error::Unauthenticated => StatusCode::UNAUTHORIZED,
105            Error::Forbidden(_) => StatusCode::FORBIDDEN,
106            Error::Validation(_) => StatusCode::UNPROCESSABLE_ENTITY,
107            Error::BadRequest(_) => StatusCode::BAD_REQUEST,
108            Error::Conflict(_) => StatusCode::CONFLICT,
109            Error::Database(sqlx::Error::RowNotFound) => StatusCode::NOT_FOUND,
110            _ => StatusCode::INTERNAL_SERVER_ERROR,
111        }
112    }
113
114    pub fn forbidden(msg: impl Into<String>) -> Self {
115        Error::Forbidden(msg.into())
116    }
117
118    pub fn bad_request(msg: impl Into<String>) -> Self {
119        Error::BadRequest(msg.into())
120    }
121
122    pub fn internal(msg: impl Into<String>) -> Self {
123        Error::Internal(msg.into())
124    }
125}
126
127impl IntoResponse for Error {
128    fn into_response(self) -> Response {
129        let status = self.status();
130        let body = match &self {
131            Error::Validation(v) => json!({
132                "message": "The given data was invalid.",
133                "errors": v.errors,
134            }),
135            other => json!({
136                "message": other.to_string(),
137            }),
138        };
139
140        if matches!(
141            self,
142            Error::Internal(_) | Error::Database(_) | Error::Other(_)
143        ) {
144            tracing::error!(error = %self, "internal error response");
145        }
146
147        (status, Json(body)).into_response()
148    }
149}
150
151impl From<garde::Report> for Error {
152    fn from(report: garde::Report) -> Self {
153        let mut errors = ValidationErrors::new();
154        for (path, err) in report.iter() {
155            errors.add(path.to_string(), err.to_string());
156        }
157        Error::Validation(errors)
158    }
159}
160
161impl From<cast_core::Error> for Error {
162    fn from(err: cast_core::Error) -> Self {
163        match err {
164            cast_core::Error::Sqlx(e) => Error::Database(e),
165            cast_core::Error::NotFound => Error::NotFound,
166            other => Error::Internal(other.to_string()),
167        }
168    }
169}