summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/cli.rs1
-rw-r--r--src/error/chain.rs72
-rw-r--r--src/error/mod.rs (renamed from src/error.rs)7
-rw-r--r--src/exit.rs54
-rw-r--r--src/lib.rs1
-rw-r--r--src/main.rs6
6 files changed, 136 insertions, 5 deletions
diff --git a/src/cli.rs b/src/cli.rs
index 154771b..2e66e34 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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
+ }
+ }
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 38e6bc5..56345d0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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()
}