diff options
| -rw-r--r-- | src/cli.rs | 1 | ||||
| -rw-r--r-- | src/error/chain.rs | 72 | ||||
| -rw-r--r-- | src/error/mod.rs (renamed from src/error.rs) | 7 | ||||
| -rw-r--r-- | src/exit.rs | 54 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/main.rs | 6 |
6 files changed, 136 insertions, 5 deletions
@@ -15,6 +15,7 @@ use sqlx::sqlite::SqlitePool; use tokio::net; use web_push::{IsahcWebPushClient, WebPushClient}; +pub use crate::exit::Exit; use crate::{ app::App, clock, db, routes, 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.rs b/src/error/mod.rs index 3c46097..154f98f 100644 --- a/src/error.rs +++ b/src/error/mod.rs @@ -1,10 +1,12 @@ -use std::{error, fmt}; +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 @@ -38,7 +40,8 @@ impl fmt::Display for Internal { impl IntoResponse for Internal { fn into_response(self) -> Response { let Self(id, error) = &self; - eprintln!("pilcrow: [{id}] {error}"); + 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() } } diff --git a/src/exit.rs b/src/exit.rs new file mode 100644 index 0000000..3c7b724 --- /dev/null +++ b/src/exit.rs @@ -0,0 +1,54 @@ +use std::{ + error::Error, + io, + process::{ExitCode, Termination}, +}; + +use crate::error::chain; + +/// Formats errors for display before exiting the program. +/// +/// When this is used as the return type of a program's `main`, it can be used to capture any errors +/// during the execution of the program and to display them in a readable format at exit time: +/// +/// ```no_run +/// # use std::{error::Error, io}; +/// fn main() -> pilcrow::cli::Exit<impl Error> { +/// my_complicated_task().into() +/// } +/// +/// fn my_complicated_task() -> Result<(), impl Error> { +/// Err(io::Error::other("stand-in for a real failure")) +/// } +/// ``` +/// +/// If constructed with an `Ok(())`, the resulting `Exit` indicates successful execution, and, when +/// returned from `main`, will cause the program to exit with a successful exit status. If constructed +/// with any `Err(…)` value, the resulting `Exit` indicates unsuccessful execution, and, when +/// returned from `main`, will cause the program to print the error (along with its `source()` +/// chain, if any) before exiting with an unsuccessful exit status. +pub struct Exit<E>(pub Result<(), E>); + +impl<E> From<Result<(), E>> for Exit<E> { + fn from(result: Result<(), E>) -> Self { + Self(result) + } +} + +impl<E> Termination for Exit<E> +where + E: Error, +{ + fn report(self) -> ExitCode { + let Self(result) = self; + match result { + Ok(()) => ExitCode::SUCCESS, + Err(err) => { + // if we can't write the error message to stderr, there's nothing else we can do + // instead, and we're about to exit with a failure anyway. + let _ = chain::format(&mut io::stderr().lock(), &err); + ExitCode::FAILURE + } + } + } +} @@ -12,6 +12,7 @@ mod db; mod empty; mod error; mod event; +pub mod exit; mod expire; mod id; mod invite; diff --git a/src/main.rs b/src/main.rs index f140da1..c7a3602 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ use clap::Parser; -use pilcrow::cli; +use pilcrow::cli::{self, Exit}; #[tokio::main] -async fn main() -> Result<(), impl std::error::Error> { +async fn main() -> Exit<impl std::error::Error> { let args = cli::Args::parse(); - args.run().await + args.run().await.into() } |
