Skip to content

Commit

Permalink
Make working with Errors a bit easier
Browse files Browse the repository at this point in the history
  • Loading branch information
Kijewski committed Nov 26, 2024
1 parent e31f4e2 commit 8880943
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 51 deletions.
10 changes: 3 additions & 7 deletions examples/actix-web-app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ async fn not_found_handler(req: HttpRequest) -> Result<impl Responder> {
};
Ok(HttpResponse::NotFound()
.insert_header(ContentType::html())
.body(tmpl.render().map_err(<Box<dyn std::error::Error>>::from)?))
.body(tmpl.render().map_err(|err| err.into_io_error())?))
} else {
Ok(HttpResponse::MethodNotAllowed().finish())
}
Expand Down Expand Up @@ -155,9 +155,7 @@ async fn index_handler(
name: query.name,
};
Ok(Html::new(
template
.render()
.map_err(<Box<dyn std::error::Error>>::from)?,
template.render().map_err(|err| err.into_io_error())?,
))
}

Expand Down Expand Up @@ -192,8 +190,6 @@ async fn greeting_handler(
name: query.name,
};
Ok(Html::new(
template
.render()
.map_err(<Box<dyn std::error::Error>>::from)?,
template.render().map_err(|err| err.into_io_error())?,
))
}
206 changes: 164 additions & 42 deletions rinja/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,73 +1,198 @@
use std::convert::Infallible;
use std::fmt::{self, Display};
use std::error::Error as StdError;
use std::{fmt, io};

/// The [`Result`](std::result::Result) type with [`Error`] as default error type
pub type Result<I, E = Error> = std::result::Result<I, E>;

/// rinja error type
/// rinja's error type
///
/// # Feature Interaction
///
/// If the feature `serde_json` is enabled an
/// additional error variant `Json` is added.
///
/// # Why not `failure`/`error-chain`?
///
/// Error from `error-chain` are not `Sync` which
/// can lead to problems e.g. when this is used
/// by a crate which use `failure`. Implementing
/// `Fail` on the other hand prevents the implementation
/// of `std::error::Error` until specialization lands
/// on stable. While errors impl. `Fail` can be
/// converted to a type impl. `std::error::Error`
/// using a adapter the benefits `failure` would
/// bring to this crate are small, which is why
/// `std::error::Error` was used.
/// Used as error value for e.g. [`Template::render()`][crate::Template::render()]
/// and custom filters.
#[non_exhaustive]
#[derive(Debug)]
pub enum Error {
/// formatting error
/// Generic, unspecified formatting error
Fmt,
/// an error raised by using `?` in a template
Custom(Box<dyn std::error::Error + Send + Sync>),
/// json conversion error
/// An error raised by using `?` in a template
Custom(Box<dyn StdError + Send + Sync>),
/// JSON conversion error
#[cfg(feature = "serde_json")]
Json(serde_json::Error),
}

impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
Error::Fmt => None,
Error::Custom(ref err) => Some(err.as_ref()),
impl Error {
/// Capture an [`StdError`]
#[inline]
pub fn custom(err: impl Into<Box<dyn StdError + Send + Sync>>) -> Self {
Self::Custom(err.into())
}

/// Convert this [`Error`] into a
/// <code>[Box]&lt;[StdError] + [Send] + [Sync]&gt;</code>
pub fn into_box(self) -> Box<dyn StdError + Send + Sync> {
match self {
Error::Fmt => fmt::Error.into(),
Error::Custom(err) => err,
#[cfg(feature = "serde_json")]
Error::Json(err) => err.into(),
}
}

/// Convert this [`Error`] into an [`io::Error`]
///
/// Not this error itself, but the contained [`source`][StdError::source] is returned.
pub fn into_io_error(self) -> io::Error {
io::Error::other(match self {
Error::Custom(err) => match err.downcast() {
Ok(err) => return *err,
Err(err) => err,
},
err => err.into_box(),
})
}
}

impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Error::Fmt => Some(&fmt::Error),
Error::Custom(err) => Some(err.as_ref()),
#[cfg(feature = "serde_json")]
Error::Json(ref err) => Some(err),
Error::Json(err) => Some(err),
}
}
}

impl Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Fmt => write!(formatter, "formatting error"),
Error::Custom(err) => write!(formatter, "{err}"),
Error::Fmt => fmt::Error.fmt(f),
Error::Custom(err) => err.fmt(f),
#[cfg(feature = "serde_json")]
Error::Json(err) => write!(formatter, "json conversion error: {err}"),
Error::Json(err) => err.fmt(f),
}
}
}

impl From<Error> for fmt::Error {
#[inline]
fn from(_: Error) -> Self {
Self
}
}

impl From<Error> for io::Error {
#[inline]
fn from(err: Error) -> Self {
err.into_io_error()
}
}

impl From<fmt::Error> for Error {
#[inline]
fn from(_: fmt::Error) -> Self {
Error::Fmt
}
}

impl From<Error> for fmt::Error {
/// This conversion inspects the argument and chooses the best fitting [`Error`] variant
impl From<Box<dyn StdError + Send + Sync>> for Error {
#[inline]
fn from(_: Error) -> Self {
Self
fn from(err: Box<dyn StdError + Send + Sync>) -> Self {
error_from_stderror(err, MAX_ERROR_UNWRAP_COUNT)
}
}

/// This conversion inspects the argument and chooses the best fitting [`Error`] variant
impl From<io::Error> for Error {
#[inline]
fn from(err: io::Error) -> Self {
from_from_io_error(err, MAX_ERROR_UNWRAP_COUNT)
}
}

const MAX_ERROR_UNWRAP_COUNT: usize = 5;

fn error_from_stderror(err: Box<dyn StdError + Send + Sync>, unwraps: usize) -> Error {
let Some(unwraps) = unwraps.checked_sub(1) else {
return Error::Custom(err);
};
match ErrorKind::inspect(err.as_ref()) {
ErrorKind::Fmt => Error::Fmt,
ErrorKind::Custom => Error::Custom(err),
#[cfg(feature = "serde_json")]
ErrorKind::Json => match err.downcast() {
Ok(err) => Error::Json(*err),
Err(_) => Error::Fmt, // unreachable
},
ErrorKind::Io => match err.downcast() {
Ok(err) => from_from_io_error(*err, unwraps),
Err(_) => Error::Fmt, // unreachable
},
ErrorKind::Rinja => match err.downcast() {
Ok(err) => *err,
Err(_) => Error::Fmt, // unreachable
},
}
}

fn from_from_io_error(err: io::Error, unwraps: usize) -> Error {
let Some(inner) = err.get_ref() else {
return Error::custom(err);
};
let Some(unwraps) = unwraps.checked_sub(1) else {
return match err.into_inner() {
Some(err) => Error::Custom(err),
None => Error::Fmt, // unreachable
};
};
match ErrorKind::inspect(inner) {
ErrorKind::Fmt => Error::Fmt,
ErrorKind::Rinja => match err.downcast() {
Ok(err) => err,
Err(_) => Error::Fmt, // unreachable
},
#[cfg(feature = "serde_json")]
ErrorKind::Json => match err.downcast() {
Ok(err) => Error::Json(err),
Err(_) => Error::Fmt, // unreachable
},
ErrorKind::Custom => match err.into_inner() {
Some(err) => Error::Custom(err),
None => Error::Fmt, // unreachable
},
ErrorKind::Io => match err.downcast() {
Ok(inner) => from_from_io_error(inner, unwraps),
Err(_) => Error::Fmt, // unreachable
},
}
}

enum ErrorKind {
Fmt,
Custom,
#[cfg(feature = "serde_json")]
Json,
Io,
Rinja,
}

impl ErrorKind {
fn inspect(err: &(dyn StdError + 'static)) -> ErrorKind {
if err.is::<fmt::Error>() {
ErrorKind::Fmt
} else if err.is::<io::Error>() {
ErrorKind::Io
} else if err.is::<Error>() {
ErrorKind::Rinja
} else {
#[cfg(feature = "serde_json")]
if err.is::<serde_json::Error>() {
return ErrorKind::Json;
}
ErrorKind::Custom
}
}
}

Expand All @@ -87,10 +212,7 @@ impl From<Infallible> for Error {
}

#[cfg(test)]
mod tests {
use super::Error;

#[allow(dead_code)]
const _: () = {
trait AssertSendSyncStatic: Send + Sync + 'static {}
impl AssertSendSyncStatic for Error {}
}
};
8 changes: 8 additions & 0 deletions rinja/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,11 @@ impl<L: FastWritable, R: FastWritable> FastWritable for Concat<L, R> {
self.1.write_into(dest)
}
}

#[inline]
pub fn map_try<T, E>(result: Result<T, E>) -> Result<T, crate::Error>
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
result.map_err(crate::Error::custom)
}
4 changes: 2 additions & 2 deletions rinja_derive/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1493,9 +1493,9 @@ impl<'a, 'h> Generator<'a, 'h> {
buf: &mut Buffer,
expr: &WithSpan<'_, Expr<'_>>,
) -> Result<DisplayWrap, CompileError> {
buf.write("rinja::helpers::core::result::Result::map_err(");
buf.write("rinja::helpers::map_try(");
self.visit_expr(ctx, buf, expr)?;
buf.write(", |err| rinja::shared::Error::Custom(rinja::helpers::core::convert::Into::into(err)))?");
buf.write(")?");
Ok(DisplayWrap::Unwrapped)
}

Expand Down

0 comments on commit 8880943

Please sign in to comment.