diff options
Diffstat (limited to 'src/error')
| -rw-r--r-- | src/error/chain.rs | 72 | ||||
| -rw-r--r-- | src/error/failed.rs | 155 | ||||
| -rw-r--r-- | src/error/mod.rs | 79 |
3 files changed, 306 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/failed.rs b/src/error/failed.rs new file mode 100644 index 0000000..4d55552 --- /dev/null +++ b/src/error/failed.rs @@ -0,0 +1,155 @@ +use std::{borrow::Cow, error::Error, fmt}; + +use super::BoxedError; + +// Don't bother allocating a String for each error if we have a static error message, which is +// a fairly common use case. +pub type Message = Cow<'static, str>; + +// Intended to represent a generic failure caused by some underlying cause, tagged with a +// context-appropriate message to help operators and developers diagnose and repair the issue. +// Unlike `Internal`, this error type has no default HTTP representation - it's not meant for that +// use case: the intended audience for a `Failed` and its underlying cause is the service operator, +// not the conversational users or client developers. +// +// The underlying cause will be used as the `Failed` error's `source()`, and the provided message +// will be used as its `Display` implementation. +// +// Ways to make a `Failed`: +// +// * Call `ResultExt::fail` or `ResultExt::fail_with` on a result to convert its error type (most +// preferred). +// * Call `ErrorExt::fail` to convert an existing error into `Failed`. +// * Call `Result::map_err` with `Failed::with_message` or `Failed::with_message_from` to convert +// its error type to `Failed`. +// * Call `Failed::new` with a message and an existing error (least preferred). +// +// Ways to use `Failed`: +// +// * As the error type of a `Result` return value, when all errors that can arise in a function +// are of interest to the operator only but there are multiple failure types to contend with. +// * As the error type of a `Result` return value, when there's only one error that can arise but +// you want to annotate that error with a contextual message, like what the function was doing at +// the time. +// * As a variant in am error enum, when _some_ errors are only of operator interest but other +// errors may need to be broken out for matching, or returned to clients. +// +// When not to use `Failed`: +// +// * When the caller needs to match on the specific error type. `Failed`, by design, assumes that +// nothing up the call stack will care about the structure of the error, and while you _can_ get +// at the underlying error to check what you ahve by going through the `source()` chain, it's +// tricky to do well. If the specific error is relevant to the caller, surface it through the +// error type, instead. +#[derive(Debug)] +pub struct Failed { + source: BoxedError, + message: Message, +} + +impl Failed { + pub fn new<M, E>(message: M, source: E) -> Self + where + M: Into<Message>, + E: Into<BoxedError>, + { + Self { + source: source.into(), + message: message.into(), + } + } +} + +// Adapters for use with `Result::map_err`. +impl Failed { + // The returned adaptor wraps an error with `Failed`, using the provided message. + pub fn with_message<M, E>(message: M) -> impl FnOnce(E) -> Self + where + M: Into<Message>, + E: Into<BoxedError>, + { + |source| Self::new(message, source) + } + + // The returned adaptor wraps an error with `Failed`, using a message from the provided message + // callback. + pub fn with_message_from<F, M, E>(message_fn: F) -> impl FnOnce(E) -> Self + where + F: FnOnce() -> M, + M: Into<Message>, + E: Into<BoxedError>, + { + |source| Self::new(message_fn(), source) + } +} + +impl Error for Failed { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(self.source.as_ref()) + } +} + +impl fmt::Display for Failed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.message.fmt(f) + } +} + +pub trait ErrorExt { + // Convenience for `Failed::new(message, err).into()`, when wrapping an existing error into + // `Failed`. + fn fail<M, T>(self, message: M) -> T + where + M: Into<Message>, + T: From<Failed>; +} + +impl<E> ErrorExt for E +where + E: Into<BoxedError>, +{ + fn fail<M, T>(self, message: M) -> T + where + M: Into<Message>, + T: From<Failed>, + { + Failed::new(message, self).into() + } +} + +pub trait ResultExt { + type Ok; + + // Convenience for `.map_err(Failed::with_message(message))`, when a result's error type can be + // collapsed into `Failed`. + fn fail<M>(self, message: M) -> Result<Self::Ok, Failed> + where + M: Into<Message>; + + fn fail_with<F, M>(self, message_fn: F) -> Result<Self::Ok, Failed> + where + F: FnOnce() -> M, + M: Into<Message>; +} + +impl<T, E> ResultExt for Result<T, E> +where + E: Into<BoxedError>, +{ + type Ok = T; + + fn fail<M>(self, message: M) -> Result<T, Failed> + where + M: Into<Message>, + { + self.map_err(Failed::with_message(message)) + } + + fn fail_with<F, M>(self, message_fn: F) -> Result<T, Failed> + where + F: FnOnce() -> M, + M: Into<Message>, + { + self.map_err(Failed::with_message_from(message_fn)) + } +} diff --git a/src/error/mod.rs b/src/error/mod.rs new file mode 100644 index 0000000..606e344 --- /dev/null +++ b/src/error/mod.rs @@ -0,0 +1,79 @@ +use std::{error, fmt, io}; + +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; + +pub mod chain; +pub mod failed; + +// 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() + } +} |
