1
2use std::fmt;
3
4use anyhow::Context;
5use axum::http::StatusCode;
6use axum::response::IntoResponse;
7use axum::Json;
8use serde::{Deserialize, Serialize};
9use utoipa::ToSchema;
10
11#[derive(Debug)]
13struct NotFound {
14 resources: Vec<String>,
15 message: String,
16}
17
18impl fmt::Display for NotFound {
19 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
20 if self.resources.is_empty() {
21 write!(f, "not found: {}", self.message)?;
22 } else {
23 f.write_str("not found [resources=")?;
24 let mut iter = self.resources.iter().peekable();
25 while let Some(r) = iter.next() {
26 if iter.peek().is_some() {
27 write!(f, "{},", r)?;
28 } else {
29 write!(f, "{}", r)?;
30 }
31 }
32 write!(f, "]: {}", self.message)?;
33 }
34 Ok(())
35 }
36}
37
38impl NotFound {
39 fn new<I: fmt::Display>(
40 resources: impl IntoIterator<Item = I>,
41 message: impl fmt::Display,
42 ) -> Self {
43 NotFound {
44 resources: resources.into_iter().map(|r| r.to_string()).collect(),
45 message: message.to_string(),
46 }
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
51pub struct NotFoundError {
52 pub resources: Vec<String>,
53 pub message: String,
54}
55
56#[allow(unused)]
57macro_rules! not_found {
58 ($ids:expr, $($arg:tt)*) => {
59 return Err($crate::error::ErrorResponse::NotFound($crate::error::NotFoundError {
60 resources: ($ids).into_iter().map(|r| r.to_string()).collect(),
61 message: format!($($arg)*),
62 }))
63 };
64}
65pub(crate) use not_found;
66
67
68#[derive(Debug)]
70struct BadRequest {
71 message: String,
72}
73
74impl fmt::Display for BadRequest {
75 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
76 write!(f, "bad request: {}", self.message)
77 }
78}
79
80impl BadRequest {
81 pub fn new(message: impl fmt::Display) -> Self {
82 BadRequest {
83 message: message.to_string(),
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
89pub struct BadRequestError {
90 pub message: String,
91}
92
93macro_rules! badarg {
94 ($($arg:tt)*) => {
95 return Err($crate::error::ErrorResponse::BadRequest($crate::error::BadRequestError {
96 message: format!($($arg)*),
97 }))
98 };
99}
100pub(crate) use badarg;
101
102
103#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
104pub struct UnauthorizedError {
105 pub message: String,
106}
107
108#[allow(unused)]
109macro_rules! unauthorized {
110 ($($arg:tt)*) => {
111 return Err($crate::error::ErrorResponse::Unauthorized($crate::error::UnauthorizedError {
112 message: format!($($arg)*),
113 }))
114 };
115}
116pub(crate) use unauthorized;
117
118#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
119pub struct InternalServerError {
120 pub message: String,
121}
122
123#[derive(Debug, Clone, Serialize, ToSchema)]
125#[serde(untagged)]
126pub enum ErrorResponse {
127 Unauthorized(UnauthorizedError),
128 BadRequest(BadRequestError),
129 NotFound(NotFoundError),
130 Internal(InternalServerError),
131}
132
133impl ErrorResponse {
134 pub fn status_code(&self) -> StatusCode {
135 match self {
136 Self::Unauthorized(_) => StatusCode::UNAUTHORIZED,
137 Self::BadRequest(_) => StatusCode::BAD_REQUEST,
138 Self::NotFound(_) => StatusCode::NOT_FOUND,
139 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
140 }
141 }
142}
143
144impl From<anyhow::Error> for ErrorResponse {
145 fn from(error: anyhow::Error) -> Self {
146 if let Some(c) = error.downcast_ref::<NotFound>() {
147 Self::NotFound(NotFoundError {
148 resources: c.resources.clone(),
149 message: format!("{:#}", error),
150 })
151 } else if error.is::<BadRequest>() {
152 Self::BadRequest(BadRequestError {
153 message: format!("{:#}", error),
154 })
155 } else {
156 Self::Internal(InternalServerError {
157 message: format!("{:#}", error),
158 })
159 }
160 }
161}
162
163impl IntoResponse for ErrorResponse {
164 fn into_response(self) -> axum::response::Response {
165 (self.status_code(), Json(self)).into_response()
166 }
167}
168
169pub trait ContextExt<T, E>: Context<T, E> {
171 fn badarg<C>(self, context: C) -> anyhow::Result<T>
172 where C: fmt::Display + Send + Sync + 'static;
173
174 fn not_found<I, V, C>(self, ids: V, context: C) -> anyhow::Result<T>
175 where
176 V: IntoIterator<Item = I>,
177 I: fmt::Display,
178 C: fmt::Display + Send + Sync + 'static;
179}
180
181
182impl<R, T, E> ContextExt<T, E> for R
183where
184 R: Context<T, E>,
185{
186 fn badarg<C>(self, context: C) -> anyhow::Result<T>
187 where
188 C: fmt::Display + Send + Sync + 'static,
189 {
190 self.context(BadRequest::new(context))
191 }
192
193 fn not_found<I, V, C>(self, ids: V, context: C) -> anyhow::Result<T>
194 where
195 V: IntoIterator<Item = I>,
196 I: fmt::Display,
197 C: fmt::Display + Send + Sync + 'static,
198 {
199 self.context(NotFound::new(ids, context))
200 }
201}
202
203pub async fn route_not_found(path: String) -> (StatusCode, Json<String>) {
205 (StatusCode::NOT_FOUND, Json(format!("path not round: {}", path)))
206}
207
208pub type HandlerResult<T> = Result<T, ErrorResponse>;