diff --git a/examples/actix-web-app/src/main.rs b/examples/actix-web-app/src/main.rs index 9a2218e2..5c5cc551 100644 --- a/examples/actix-web-app/src/main.rs +++ b/examples/actix-web-app/src/main.rs @@ -96,7 +96,7 @@ async fn not_found_handler(req: HttpRequest) -> Result { }; Ok(HttpResponse::NotFound() .insert_header(ContentType::html()) - .body(tmpl.render().map_err(>::from)?)) + .body(tmpl.render().map_err(|err| err.into_io_error())?)) } else { Ok(HttpResponse::MethodNotAllowed().finish()) } @@ -155,9 +155,7 @@ async fn index_handler( name: query.name, }; Ok(Html::new( - template - .render() - .map_err(>::from)?, + template.render().map_err(|err| err.into_io_error())?, )) } @@ -192,8 +190,6 @@ async fn greeting_handler( name: query.name, }; Ok(Html::new( - template - .render() - .map_err(>::from)?, + template.render().map_err(|err| err.into_io_error())?, )) } diff --git a/rinja/src/error.rs b/rinja/src/error.rs index 37d12d38..66616cb3 100644 --- a/rinja/src/error.rs +++ b/rinja/src/error.rs @@ -1,62 +1,94 @@ 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 = std::result::Result; -/// 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), - /// json conversion error + /// An error raised by using `?` in a template + Custom(Box), + /// 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>) -> Self { + Self::Custom(err.into()) + } + + /// Convert this [`Error`] into a + /// [Box]<dyn [StdError] + [Send] + [Sync]> + pub fn into_box(self) -> Box { + match self { + Error::Fmt => fmt::Error.into(), + Error::Custom(err) => err, #[cfg(feature = "serde_json")] - Error::Json(ref err) => Some(err), + 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(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 for fmt::Error { + #[inline] + fn from(_: Error) -> Self { + Self + } +} + +impl From for io::Error { + #[inline] + fn from(err: Error) -> Self { + err.into_io_error() + } +} + impl From for Error { #[inline] fn from(_: fmt::Error) -> Self { @@ -64,10 +96,103 @@ impl From for Error { } } -impl From for fmt::Error { +/// This conversion inspects the argument and chooses the best fitting [`Error`] variant +impl From> for Error { #[inline] - fn from(_: Error) -> Self { - Self + fn from(err: Box) -> Self { + error_from_stderror(err, MAX_ERROR_UNWRAP_COUNT) + } +} + +/// This conversion inspects the argument and chooses the best fitting [`Error`] variant +impl From 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, 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::() { + ErrorKind::Fmt + } else if err.is::() { + ErrorKind::Io + } else if err.is::() { + ErrorKind::Rinja + } else { + #[cfg(feature = "serde_json")] + if err.is::() { + return ErrorKind::Json; + } + ErrorKind::Custom + } } } @@ -87,10 +212,7 @@ impl From for Error { } #[cfg(test)] -mod tests { - use super::Error; - - #[allow(dead_code)] +const _: () = { trait AssertSendSyncStatic: Send + Sync + 'static {} impl AssertSendSyncStatic for Error {} -} +}; diff --git a/rinja/src/helpers.rs b/rinja/src/helpers.rs index bf345599..5669909d 100644 --- a/rinja/src/helpers.rs +++ b/rinja/src/helpers.rs @@ -263,3 +263,11 @@ impl FastWritable for Concat { self.1.write_into(dest) } } + +#[inline] +pub fn map_try(result: Result) -> Result +where + E: Into>, +{ + result.map_err(crate::Error::custom) +} diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs index d3de5620..96bd7f4a 100644 --- a/rinja_derive/src/generator.rs +++ b/rinja_derive/src/generator.rs @@ -1493,9 +1493,9 @@ impl<'a, 'h> Generator<'a, 'h> { buf: &mut Buffer, expr: &WithSpan<'_, Expr<'_>>, ) -> Result { - 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) }