diff options
Diffstat (limited to 'src/error')
| -rw-r--r-- | src/error/chain.rs | 72 | ||||
| -rw-r--r-- | src/error/mod.rs | 78 |
2 files changed, 150 insertions, 0 deletions
diff --git a/src/error/chain.rs b/src/error/chain.rs new file mode 100644 index 0000000..d78fe67 --- /dev/null +++ b/src/error/chain.rs @@ -0,0 +1,72 @@ +use std::{error::Error, io}; + +use super::Id; + +pub fn format<W, E>(out: &mut W, error: E) -> Result<(), io::Error> +where + W: io::Write, + E: Error, +{ + writeln!(out, "{error}")?; + format_sources(out, error)?; + + Ok(()) +} + +pub fn format_with_id<W, E>(out: &mut W, id: &Id, error: E) -> Result<(), io::Error> +where + W: io::Write, + E: Error, +{ + writeln!(out, "[{id}] {error}")?; + format_sources(out, error)?; + + Ok(()) +} + +fn format_sources<W, E>(out: &mut W, error: E) -> Result<(), io::Error> +where + W: io::Write, + E: Error, +{ + let mut sources = Sources::from(&error); + if let Some(source) = sources.next() { + writeln!(out)?; + writeln!(out, "Caused by:")?; + writeln!(out, " {source}")?; + for source in sources { + writeln!(out, " {source}")?; + } + writeln!(out)?; + } + Ok(()) +} + +struct Sources<'e> { + next: Option<&'e dyn Error>, +} + +impl<'e, E> From<&'e E> for Sources<'e> +where + E: Error, +{ + fn from(error: &'e E) -> Self { + Self { + next: error.source(), + } + } +} + +// See also: <https://doc.rust-lang.org/std/error/trait.Error.html#method.sources> However, we only +// want to iterate the sources, and not the error itself. Personally, I find the `skip(1)` +// suggestion untidy, since the error itself is non-optional while the sources are optional. +impl<'a> Iterator for Sources<'a> { + type Item = &'a dyn Error; + + fn next(&mut self) -> Option<Self::Item> { + let source = self.next; + self.next = self.next.and_then(|err| err.source()); + + source + } +} diff --git a/src/error/mod.rs b/src/error/mod.rs new file mode 100644 index 0000000..154f98f --- /dev/null +++ b/src/error/mod.rs @@ -0,0 +1,78 @@ +use std::{error, fmt, io}; + +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; + +pub mod chain; + +// I'm making an effort to avoid `anyhow` here, as that crate is _enormously_ +// complex (though very usable). We don't need to be overly careful about +// allocations on errors in this app, so this is fine for most "general +// failure" cases. +type BoxedError = Box<dyn error::Error + Send + Sync>; + +// Returns a 500 Internal Server Error to the client. Meant to be used via the +// `?` operator; _does not_ return the originating error to the client. +#[derive(Debug)] +pub struct Internal(Id, BoxedError); + +impl<E> From<E> for Internal +where + E: Into<BoxedError>, +{ + fn from(error: E) -> Self { + let id = Id::generate(); + Self(id, error.into()) + } +} + +impl fmt::Display for Internal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self(id, _) = self; + writeln!(f, "internal server error")?; + writeln!(f, "error id: {id}")?; + Ok(()) + } +} + +impl IntoResponse for Internal { + fn into_response(self) -> Response { + let Self(id, error) = &self; + chain::format_with_id(&mut io::stderr().lock(), id, error.as_ref()) + .expect("write to stderr"); + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() + } +} + +pub type Id = crate::id::Id<InternalError>; + +#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct InternalError; + +impl crate::id::Prefix for InternalError { + fn prefix(&self) -> &'static str { + "E" + } +} + +pub struct Unauthorized; + +impl IntoResponse for Unauthorized { + fn into_response(self) -> Response { + (StatusCode::UNAUTHORIZED, "unauthorized").into_response() + } +} + +pub struct NotFound<E>(pub E); + +impl<E> IntoResponse for NotFound<E> +where + E: std::error::Error, +{ + fn into_response(self) -> Response { + let Self(response) = self; + (StatusCode::NOT_FOUND, response.to_string()).into_response() + } +} |
