1use 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}