Skip to main content

bark_rest/
error.rs

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/// a NOT_FOUND anyhow context object
12#[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/// a BAD_REQUEST anyhow context object
69#[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// NB since we don't do any tagging, this is not deserializable
124#[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
169/// Extension trait for adding bark-server-specific error info.
170pub 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
203// 404 handler for unmatched routes
204pub async fn route_not_found(path: String) -> (StatusCode, Json<String>) {
205	(StatusCode::NOT_FOUND, Json(format!("path not round: {}", path)))
206}
207
208// Convenience type alias for handlers that return anyhow::Result
209pub type HandlerResult<T> = Result<T, ErrorResponse>;