use std::fmt;
use crate::types::InteractionResponse;
pub type ErrorRenderer = fn(&HandlerError) -> InteractionResponse;
#[derive(Debug)]
pub struct HandlerError {
kind: ErrorKind,
message: String,
sources: Box<[String]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
Handler,
User,
UnknownCommand,
InvalidOption,
}
impl HandlerError {
pub fn new(message: impl fmt::Display) -> Self {
Self::of_kind(ErrorKind::Handler, message.to_string())
}
pub fn user(message: impl fmt::Display) -> Self {
Self::of_kind(ErrorKind::User, message.to_string())
}
pub fn unknown_command(name: &str) -> Self {
Self::of_kind(
ErrorKind::UnknownCommand,
format!("unknown command `{name}`"),
)
}
pub fn invalid_option(name: &str, error: impl fmt::Display) -> Self {
Self::of_kind(
ErrorKind::InvalidOption,
format!("option `{name}`: {error}"),
)
}
fn of_kind(kind: ErrorKind, message: String) -> Self {
Self {
kind,
message,
sources: Box::default(),
}
}
pub fn with_source(self, source: impl std::error::Error) -> Self {
let mut sources = self.sources.into_vec();
sources.push(source.to_string());
Self {
sources: chain(sources, source.source()),
..self
}
}
pub fn kind(&self) -> ErrorKind {
self.kind
}
pub fn sources(&self) -> impl Iterator<Item = &str> {
self.sources.iter().map(String::as_str)
}
#[cfg(feature = "tracing")]
fn sources_field(&self) -> Option<tracing::field::DisplayValue<Sources<'_>>> {
(!self.sources.is_empty()).then(|| tracing::field::display(Sources(&self.sources)))
}
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
pub(crate) fn trace(&self, message: &'static str) {
match self.kind {
ErrorKind::User => {
debug!(error = %self, sources = self.sources_field(), error_kind = "user", "{message}");
}
ErrorKind::UnknownCommand => {
warn!(error = %self, sources = self.sources_field(), error_kind = "unknown_command", "{message}");
}
ErrorKind::InvalidOption => {
warn!(error = %self, sources = self.sources_field(), error_kind = "invalid_option", "{message}");
}
ErrorKind::Handler => {
error!(error = %self, sources = self.sources_field(), error_kind = "handler", "{message}");
}
}
}
pub fn response(&self) -> InteractionResponse {
InteractionResponse::respond_with_ephemeral_message(self.user_message().to_string())
}
pub fn user_message(&self) -> &str {
match self.kind {
ErrorKind::User => &self.message,
ErrorKind::UnknownCommand => "Unknown command.",
ErrorKind::Handler | ErrorKind::InvalidOption => {
"Something went wrong handling this interaction."
}
}
}
}
impl fmt::Display for HandlerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)?;
if f.alternate() && !self.sources.is_empty() {
write!(f, ": {}", Sources(&self.sources))?;
}
Ok(())
}
}
struct Sources<'a>(&'a [String]);
impl fmt::Display for Sources<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, source) in self.0.iter().enumerate() {
if i > 0 {
f.write_str(": ")?;
}
f.write_str(source)?;
}
Ok(())
}
}
fn chain(
mut sources: Vec<String>,
error: Option<&(dyn std::error::Error + 'static)>,
) -> Box<[String]> {
let causes = || std::iter::successors(error, |error| error.source());
sources.reserve_exact(causes().count());
sources.extend(causes().map(ToString::to_string));
sources.into_boxed_slice()
}
impl<E: std::error::Error> From<E> for HandlerError {
fn from(error: E) -> Self {
Self {
sources: chain(Vec::new(), error.source()),
..Self::new(error)
}
}
}
#[cfg(test)]
mod tests {
use std::{error::Error, io};
use super::*;
#[derive(Debug)]
struct Failure(&'static str, Option<Box<Failure>>);
impl fmt::Display for Failure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
impl Error for Failure {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.1.as_deref().map(|source| source as _)
}
}
fn nested() -> Failure {
Failure(
"request failed",
Some(Box::new(Failure(
"connection reset",
Some(Box::new(Failure("os error 104", None))),
))),
)
}
#[test]
fn converting_keeps_the_source_chain() {
let error = HandlerError::from(nested());
assert_eq!(error.kind(), ErrorKind::Handler);
assert_eq!(error.to_string(), "request failed");
assert_eq!(
error.sources().collect::<Vec<_>>(),
["connection reset", "os error 104"]
);
assert_eq!(
format!("{error:#}"),
"request failed: connection reset: os error 104"
);
}
#[test]
fn io_errors_keep_their_payloads_chain() {
let error = HandlerError::from(io::Error::other(nested()));
assert_eq!(error.to_string(), "request failed");
assert_eq!(
format!("{error:#}"),
"request failed: connection reset: os error 104"
);
}
#[test]
fn question_mark_converts() {
fn handler() -> Result<(), HandlerError> {
Err(nested())?
}
assert_eq!(handler().unwrap_err().sources().count(), 2);
}
#[test]
fn with_source_wraps_by_hand() {
let error = HandlerError::user("Couldn't save your note").with_source(nested());
assert_eq!(error.kind(), ErrorKind::User);
assert_eq!(error.to_string(), "Couldn't save your note");
assert_eq!(
format!("{error:#}"),
"Couldn't save your note: request failed: connection reset: os error 104"
);
let response = serde_json::to_value(error.response()).unwrap();
assert_eq!(response["data"]["content"], "Couldn't save your note");
}
#[test]
fn without_sources_alternate_is_plain() {
let error = HandlerError::new("boom");
assert_eq!(error.sources().count(), 0);
assert_eq!(format!("{error:#}"), "boom");
}
}