1use axum::http::StatusCode;
45use axum::response::{IntoResponse, Response};
46use axum::Json;
47use serde_json::json;
48use thiserror::Error;
49
50pub type Result<T, E = Error> = std::result::Result<T, E>;
51
52#[derive(Debug, Error)]
53pub enum Error {
54 #[error("not found")]
55 NotFound,
56
57 #[error("unauthenticated")]
58 Unauthenticated,
59
60 #[error("forbidden: {0}")]
61 Forbidden(String),
62
63 #[error("validation failed")]
64 Validation(ValidationErrors),
65
66 #[error("bad request: {0}")]
67 BadRequest(String),
68
69 #[error("conflict: {0}")]
70 Conflict(String),
71
72 #[error("database error: {0}")]
73 Database(#[from] sqlx::Error),
74
75 #[error("io error: {0}")]
76 Io(#[from] std::io::Error),
77
78 #[error("serialization error: {0}")]
79 Serialization(#[from] serde_json::Error),
80
81 #[error("config error: {0}")]
82 Config(String),
83
84 #[error("template error: {0}")]
85 Template(String),
86
87 #[error("queue error: {0}")]
88 Queue(String),
89
90 #[error("mail error: {0}")]
91 Mail(String),
92
93 #[error("cache error: {0}")]
94 Cache(String),
95
96 #[error("storage error: {0}")]
97 Storage(String),
98
99 #[error("internal server error: {0}")]
100 Internal(String),
101
102 #[error("{0}")]
103 Other(#[from] anyhow::Error),
104}
105
106#[derive(Debug, Clone, Default, serde::Serialize)]
107pub struct ValidationErrors {
108 pub errors: indexmap::IndexMap<String, Vec<String>>,
109}
110
111impl ValidationErrors {
112 pub fn new() -> Self {
113 Self {
114 errors: indexmap::IndexMap::new(),
115 }
116 }
117
118 pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
119 self.errors
120 .entry(field.into())
121 .or_default()
122 .push(message.into());
123 }
124
125 pub fn is_empty(&self) -> bool {
126 self.errors.is_empty()
127 }
128}
129
130impl std::fmt::Display for ValidationErrors {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 for (field, msgs) in &self.errors {
133 for msg in msgs {
134 writeln!(f, "{field}: {msg}")?;
135 }
136 }
137 Ok(())
138 }
139}
140
141impl Error {
142 pub fn status(&self) -> StatusCode {
143 match self {
144 Error::NotFound => StatusCode::NOT_FOUND,
145 Error::Unauthenticated => StatusCode::UNAUTHORIZED,
146 Error::Forbidden(_) => StatusCode::FORBIDDEN,
147 Error::Validation(_) => StatusCode::UNPROCESSABLE_ENTITY,
148 Error::BadRequest(_) => StatusCode::BAD_REQUEST,
149 Error::Conflict(_) => StatusCode::CONFLICT,
150 Error::Database(sqlx::Error::RowNotFound) => StatusCode::NOT_FOUND,
151 _ => StatusCode::INTERNAL_SERVER_ERROR,
152 }
153 }
154
155 pub fn forbidden(msg: impl Into<String>) -> Self {
156 Error::Forbidden(msg.into())
157 }
158
159 pub fn bad_request(msg: impl Into<String>) -> Self {
160 Error::BadRequest(msg.into())
161 }
162
163 pub fn internal(msg: impl Into<String>) -> Self {
164 Error::Internal(msg.into())
165 }
166}
167
168impl IntoResponse for Error {
169 fn into_response(self) -> Response {
170 let status = self.status();
171 let body = match &self {
172 Error::Validation(v) => json!({
173 "message": "The given data was invalid.",
174 "errors": v.errors,
175 }),
176 other => json!({
177 "message": other.to_string(),
178 }),
179 };
180
181 if matches!(
182 self,
183 Error::Internal(_) | Error::Database(_) | Error::Other(_)
184 ) {
185 tracing::error!(error = %self, "internal error response");
186 }
187
188 (status, Json(body)).into_response()
189 }
190}
191
192impl From<garde::Report> for Error {
193 fn from(report: garde::Report) -> Self {
194 let mut errors = ValidationErrors::new();
195 for (path, err) in report.iter() {
196 errors.add(path.to_string(), err.to_string());
197 }
198 Error::Validation(errors)
199 }
200}
201
202impl From<cast_core::Error> for Error {
203 fn from(err: cast_core::Error) -> Self {
204 match err {
205 cast_core::Error::Sqlx(e) => Error::Database(e),
206 cast_core::Error::NotFound => Error::NotFound,
207 other => Error::Internal(other.to_string()),
208 }
209 }
210}