Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make working with Errors a bit easier #277

Merged
merged 1 commit into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;dyn [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