Repository: rinja-rs/askama (language: rust)

Issue:
Unify integration API
There is

* [`fn askama_warp::reply(&impl Template) -> Response`](https://github.com/djc/askama/blob/178a6895d47a397e8d6bffcbc34c9782b08b0b8c/askama_warp/src/lib.rs)
* [`fn askama_tide::try_into_body(&impl Template) -> Result<Body>`](https://github.com/djc/askama/blob/178a6895d47a397e8d6bffcbc34c9782b08b0b8c/askama_tide/src/lib.rs)
* [`fn askama_tide::into_response(&impl Template) -> Response`](https://github.com/djc/askama/blob/178a6895d47a397e8d6bffcbc34c9782b08b0b8c/askama_tide/src/lib.rs)
* [`fn askama_rocket::respond(&impl Template) -> Result<_>`](https://github.com/djc/askama/blob/178a6895d47a397e8d6bffcbc34c9782b08b0b8c/askama_rocket/src/lib.rs)
* [`fn askama_axum::into_response(&impl Template) -> Response`](https://github.com/djc/askama/blob/178a6895d47a397e8d6bffcbc34c9782b08b0b8c/askama_axum/src/lib.rs)
* [`<T as TemplateToResponse>::to_response(&self) -> Response`](https://github.com/djc/askama/blob/178a6895d47a397e8d6bffcbc34c9782b08b0b8c/askama_actix/src/lib.rs)

Shouldn't we use the moment, if we upload new versions of the integration crates anyway, to unify the APIs amongst the crates?

I like the API of `askama_tide` the best: One function `try_into_response(&impl Template)` to generate a `Result<_>`, and one function `into_response(&impl Template)` to generate a response that might be an error page.


Discussion / hints:
Sounds good to me!

Fix patch:
diff --git a/README.md b/README.md
index f394dfb31..776abf687 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ in a for-profit context, please consider supporting my open source work on
 * Construct templates using a familiar, easy-to-use syntax
 * Benefit from the safety provided by Rust's type system
 * Template code is compiled into your crate for [optimal performance][benchmarks]
-* Optional built-in support for Actix, Axum, Gotham, Mendes, Rocket, tide, and warp web frameworks
+* Optional built-in support for Actix, Axum, Rocket, and warp web frameworks
 * Debugging features to assist you in template development
 * Templates must be valid UTF-8 and produce UTF-8 when rendered
 * IDE support available in [JetBrains products](https://plugins.jetbrains.com/plugin/16591-askama-template-support)
diff --git a/askama_actix/src/lib.rs b/askama_actix/src/lib.rs
index d98debcf2..67f160e6f 100644
--- a/askama_actix/src/lib.rs
+++ b/askama_actix/src/lib.rs
@@ -4,13 +4,30 @@
 
 use std::fmt;
 
+#[doc(no_inline)]
 pub use actix_web;
 use actix_web::body::BoxBody;
 use actix_web::http::header::HeaderValue;
 use actix_web::http::StatusCode;
 use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError};
+#[doc(no_inline)]
 pub use askama::*;
 
+/// Render a [`Template`] into a [`HttpResponse`], or render an error page.
+pub fn into_response<T: ?Sized + askama::Template>(tmpl: &T) -> HttpResponse<BoxBody> {
+    try_into_response(tmpl).unwrap_or_else(|err| HttpResponse::from_error(ActixError(err)))
+}
+
+/// Try to render a [`Template`] into a [`HttpResponse`].
+pub fn try_into_response<T: ?Sized + askama::Template>(
+    tmpl: &T,
+) -> Result<HttpResponse<BoxBody>, Error> {
+    let value = tmpl.render()?;
+    Ok(HttpResponseBuilder::new(StatusCode::OK)
+        .content_type(HeaderValue::from_static(T::MIME_TYPE))
+        .body(value))
+}
+
 /// Newtype to let askama::Error implement actix_web::ResponseError.
 struct ActixError(Error);
 
@@ -29,18 +46,3 @@ impl fmt::Display for ActixError {
 }
 
 impl ResponseError for ActixError {}
-
-pub trait TemplateToResponse {
-    fn to_response(&self) -> HttpResponse<BoxBody>;
-}
-
-impl<T: askama::Template> TemplateToResponse for T {
-    fn to_response(&self) -> HttpResponse<BoxBody> {
-        match self.render() {
-            Ok(buffer) => HttpResponseBuilder::new(StatusCode::OK)
-                .content_type(HeaderValue::from_static(T::MIME_TYPE))
-                .body(buffer),
-            Err(err) => HttpResponse::from_error(ActixError(err)),
-        }
-    }
-}
diff --git a/askama_axum/src/lib.rs b/askama_axum/src/lib.rs
index 090d1d2a9..bbc47ab18 100644
--- a/askama_axum/src/lib.rs
+++ b/askama_axum/src/lib.rs
@@ -2,20 +2,27 @@
 #![deny(elided_lifetimes_in_paths)]
 #![deny(unreachable_pub)]
 
+#[doc(no_inline)]
 pub use askama::*;
-pub use axum_core::response::{IntoResponse, Response};
-use http::StatusCode;
+#[doc(no_inline)]
+pub use axum_core;
+use axum_core::response::{IntoResponse, Response};
 
-pub fn into_response<T: Template>(t: &T) -> Response {
-    match t.render() {
-        Ok(body) => {
-            let headers = [(
-                http::header::CONTENT_TYPE,
-                http::HeaderValue::from_static(T::MIME_TYPE),
-            )];
+/// Render a [`Template`] into a [`Response`], or render an error page.
+pub fn into_response<T: ?Sized + askama::Template>(tmpl: &T) -> Response {
+    try_into_response(tmpl)
+        .map_err(|err| axum_core::response::ErrorResponse::from(err.to_string()))
+        .into_response()
+}
 
-            (headers, body).into_response()
-        }
-        Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
-    }
+/// Try to render a [`Template`] into a [`Response`].
+pub fn try_into_response<T: ?Sized + askama::Template>(tmpl: &T) -> Result<Response, Error> {
+    let value = tmpl.render()?.into();
+    Response::builder()
+        .header(
+            http::header::CONTENT_TYPE,
+            http::header::HeaderValue::from_static(T::MIME_TYPE),
+        )
+        .body(value)
+        .map_err(|err| Error::Custom(err.into()))
 }
diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs
index ddda791dd..866fca764 100644
--- a/askama_derive/src/generator.rs
+++ b/askama_derive/src/generator.rs
@@ -72,8 +72,6 @@ impl<'a> Generator<'a> {
         self.impl_axum_into_response(&mut buf)?;
         #[cfg(feature = "with-rocket")]
         self.impl_rocket_responder(&mut buf)?;
-        #[cfg(feature = "with-tide")]
-        self.impl_tide_integrations(&mut buf)?;
         #[cfg(feature = "with-warp")]
         self.impl_warp_reply(&mut buf)?;
 
@@ -157,7 +155,7 @@ impl<'a> Generator<'a> {
             "fn respond_to(self, _req: &::askama_actix::actix_web::HttpRequest) \
              -> ::askama_actix::actix_web::HttpResponse<Self::Body> {",
         )?;
-        buf.writeln("<Self as ::askama_actix::TemplateToResponse>::to_response(&self)")?;
+        buf.writeln("::askama_actix::into_response(&self)")?;
         buf.writeln("}")?;
         buf.writeln("}")
     }
@@ -165,11 +163,15 @@ impl<'a> Generator<'a> {
     // Implement Axum's `IntoResponse`.
     #[cfg(feature = "with-axum")]
     fn impl_axum_into_response(&mut self, buf: &mut Buffer) -> Result<(), CompileError> {
-        self.write_header(buf, "::askama_axum::IntoResponse", None)?;
+        self.write_header(
+            buf,
+            "::askama_axum::axum_core::response::IntoResponse",
+            None,
+        )?;
         buf.writeln("#[inline]")?;
         buf.writeln(
             "fn into_response(self)\
-             -> ::askama_axum::Response {",
+             -> ::askama_axum::axum_core::response::Response {",
         )?;
         buf.writeln("::askama_axum::into_response(&self)")?;
         buf.writeln("}")?;
@@ -180,65 +182,30 @@ impl<'a> Generator<'a> {
     #[cfg(feature = "with-rocket")]
     fn impl_rocket_responder(&mut self, buf: &mut Buffer) -> Result<(), CompileError> {
         let lifetime1 = syn::Lifetime::new("'askama1", proc_macro2::Span::call_site());
-        let lifetime2 = syn::Lifetime::new("'askama2", proc_macro2::Span::call_site());
-
-        let mut param2 = syn::LifetimeParam::new(lifetime2);
-        param2.colon_token = Some(syn::Token![:](proc_macro2::Span::call_site()));
-        param2.bounds = syn::punctuated::Punctuated::new();
-        param2.bounds.push_value(lifetime1.clone());
-
         let param1 = syn::GenericParam::Lifetime(syn::LifetimeParam::new(lifetime1));
-        let param2 = syn::GenericParam::Lifetime(param2);
 
         self.write_header(
             buf,
-            "::askama_rocket::Responder<'askama1, 'askama2>",
-            Some(vec![param1, param2]),
+            "::askama_rocket::rocket::response::Responder<'askama1, 'static>",
+            Some(vec![param1]),
         )?;
-
         buf.writeln("#[inline]")?;
         buf.writeln(
-            "fn respond_to(self, _: &'askama1 ::askama_rocket::Request) \
-             -> ::askama_rocket::RocketResult<'askama2> {",
+            "fn respond_to(self, _: &'askama1 ::askama_rocket::rocket::request::Request<'_>) \
+             -> ::askama_rocket::rocket::response::Result<'static> {",
         )?;
         buf.writeln("::askama_rocket::respond(&self)")?;
-
         buf.writeln("}")?;
         buf.writeln("}")?;
         Ok(())
     }
 
-    #[cfg(feature = "with-tide")]
-    fn impl_tide_integrations(&mut self, buf: &mut Buffer) -> Result<(), CompileError> {
-        self.write_header(
-            buf,
-            "::std::convert::TryInto<::askama_tide::tide::Body>",
-            None,
-        )?;
-        buf.writeln(
-            "type Error = ::askama_tide::Error;\n\
-            #[inline]\n\
-            fn try_into(self) -> \
-			::askama_tide::Result<::askama_tide::tide::Body> {",
-        )?;
-        buf.writeln("::askama_tide::try_into_body(&self)")?;
-        buf.writeln("}")?;
-        buf.writeln("}")?;
-
-        buf.writeln("#[allow(clippy::from_over_into)]")?;
-        self.write_header(buf, "Into<::askama_tide::tide::Response>", None)?;
-        buf.writeln("#[inline]")?;
-        buf.writeln("fn into(self) -> ::askama_tide::tide::Response {")?;
-        buf.writeln("::askama_tide::into_response(&self)")?;
-        buf.writeln("}\n}")
-    }
-
     #[cfg(feature = "with-warp")]
     fn impl_warp_reply(&mut self, buf: &mut Buffer) -> Result<(), CompileError> {
         self.write_header(buf, "::askama_warp::warp::reply::Reply", None)?;
         buf.writeln("#[inline]")?;
         buf.writeln("fn into_response(self) -> ::askama_warp::warp::reply::Response {")?;
-        buf.writeln("::askama_warp::reply(&self)")?;
+        buf.writeln("::askama_warp::into_response(&self)")?;
         buf.writeln("}")?;
         buf.writeln("}")
     }
@@ -251,24 +218,27 @@ impl<'a> Generator<'a> {
         target: &str,
         params: Option<Vec<syn::GenericParam>>,
     ) -> Result<(), CompileError> {
-        let mut generics = self.input.ast.generics.clone();
-        if let Some(params) = params {
+        let mut generics;
+        let (impl_generics, orig_ty_generics, where_clause) = if let Some(params) = params {
+            generics = self.input.ast.generics.clone();
             for param in params {
                 generics.params.push(param);
             }
-        }
-        let (_, orig_ty_generics, _) = self.input.ast.generics.split_for_impl();
-        let (impl_generics, _, where_clause) = generics.split_for_impl();
-        buf.writeln(
-            format!(
-                "{} {} for {}{} {{",
-                quote!(impl #impl_generics),
-                target,
-                self.input.ast.ident,
-                quote!(#orig_ty_generics #where_clause),
-            )
-            .as_ref(),
-        )
+
+            let (_, orig_ty_generics, _) = self.input.ast.generics.split_for_impl();
+            let (impl_generics, _, where_clause) = generics.split_for_impl();
+            (impl_generics, orig_ty_generics, where_clause)
+        } else {
+            self.input.ast.generics.split_for_impl()
+        };
+
+        buf.writeln(&format!(
+            "{} {} for {}{} {{",
+            quote!(impl #impl_generics),
+            target,
+            self.input.ast.ident,
+            quote!(#orig_ty_generics #where_clause),
+        ))
     }
 
     /* Helper methods for handling node types */
@@ -1179,7 +1149,7 @@ impl<'a> Generator<'a> {
             Expr::Group(ref inner) => self.visit_group(buf, inner)?,
             Expr::Call(ref obj, ref args) => self.visit_call(buf, obj, args)?,
             Expr::RustMacro(ref path, args) => self.visit_rust_macro(buf, path, args),
-            Expr::Try(ref expr) => self.visit_try(buf, expr.as_ref())?,
+            Expr::Try(ref expr) => self.visit_try(buf, expr)?,
             Expr::Tuple(ref exprs) => self.visit_tuple(buf, exprs)?,
             Expr::NamedArgument(_, ref expr) => self.visit_named_argument(buf, expr)?,
             Expr::Generated(ref s) => self.visit_generated(buf, s),
diff --git a/askama_rocket/src/lib.rs b/askama_rocket/src/lib.rs
index c302d19ae..52e09b701 100644
--- a/askama_rocket/src/lib.rs
+++ b/askama_rocket/src/lib.rs
@@ -4,16 +4,42 @@
 
 use std::io::Cursor;
 
+#[doc(no_inline)]
 pub use askama::*;
-use rocket::http::{Header, Status};
-pub use rocket::request::Request;
-use rocket::response::Response;
-pub use rocket::response::{Responder, Result as RocketResult};
+#[doc(no_inline)]
+pub use rocket;
+use rocket::Response;
 
-pub fn respond<T: Template>(t: &T) -> RocketResult<'static> {
-    let rsp = t.render().map_err(|_| Status::InternalServerError)?;
-    Response::build()
-        .header(Header::new("content-type", T::MIME_TYPE))
-        .sized_body(rsp.len(), Cursor::new(rsp))
-        .ok()
+#[inline]
+pub fn respond<T: ?Sized + askama::Template>(tmpl: &T) -> rocket::response::Result<'static> {
+    try_into_response(tmpl).map_err(|_| rocket::http::Status::InternalServerError)
+}
+
+/// Render a [`Template`] into a [`Response`], or render an error page.
+pub fn into_response<T: ?Sized + askama::Template>(tmpl: &T) -> Response<'static> {
+    match try_into_response(tmpl) {
+        Ok(response) => response,
+        Err(err) => {
+            let value = err.to_string();
+            Response::build()
+                .status(rocket::http::Status::InternalServerError)
+                .header(rocket::http::Header::new(
+                    "content-type",
+                    "text/plain; charset=utf-8",
+                ))
+                .sized_body(value.len(), Cursor::new(value))
+                .finalize()
+        }
+    }
+}
+
+/// Try to render a [`Template`] into a [`Response`].
+pub fn try_into_response<T: ?Sized + askama::Template>(
+    tmpl: &T,
+) -> Result<Response<'static>, Error> {
+    let value = tmpl.render()?;
+    Ok(Response::build()
+        .header(rocket::http::Header::new("content-type", T::MIME_TYPE))
+        .sized_body(value.len(), Cursor::new(value))
+        .finalize())
 }
diff --git a/askama_warp/src/lib.rs b/askama_warp/src/lib.rs
index 9c4bb9ba8..242ee1b33 100644
--- a/askama_warp/src/lib.rs
+++ b/askama_warp/src/lib.rs
@@ -2,22 +2,33 @@
 #![deny(elided_lifetimes_in_paths)]
 #![deny(unreachable_pub)]
 
+#[doc(no_inline)]
 pub use askama::*;
+#[doc(no_inline)]
 pub use warp;
-
-use warp::http::{self, header, StatusCode};
-use warp::hyper::Body;
 use warp::reply::Response;
 
-pub fn reply<T: askama::Template>(t: &T) -> Response {
-    match t.render() {
-        Ok(body) => http::Response::builder()
-            .status(StatusCode::OK)
-            .header(header::CONTENT_TYPE, T::MIME_TYPE)
-            .body(body.into()),
-        Err(_) => http::Response::builder()
-            .status(StatusCode::INTERNAL_SERVER_ERROR)
-            .body(Body::empty()),
+/// Render a [`Template`] into a [`Response`], or render an error page.
+pub fn into_response<T: ?Sized + askama::Template>(tmpl: &T) -> Response {
+    match try_into_response(tmpl) {
+        Ok(response) => response,
+        Err(err) => warp::http::Response::builder()
+            .status(warp::http::StatusCode::INTERNAL_SERVER_ERROR)
+            .header(
+                warp::http::header::CONTENT_TYPE,
+                warp::http::HeaderValue::from_static("text/plain; charset=utf-8"),
+            )
+            .body(err.to_string().into())
+            .unwrap(),
     }
-    .unwrap()
+}
+
+/// Try to render a [`Template`] into a [`Response`].
+pub fn try_into_response<T: ?Sized + askama::Template>(tmpl: &T) -> Result<Response, Error> {
+    let value = tmpl.render()?.into();
+    warp::http::Response::builder()
+        .status(warp::http::StatusCode::OK)
+        .header(warp::http::header::CONTENT_TYPE, T::MIME_TYPE)
+        .body(value)
+        .map_err(|err| Error::Custom(err.into()))
 }


Test patch:
diff --git a/askama_actix/tests/basic.rs b/askama_actix/tests/basic.rs
index 615e32446..58e3e1979 100644
--- a/askama_actix/tests/basic.rs
+++ b/askama_actix/tests/basic.rs
@@ -1,6 +1,6 @@
 use actix_web::http::header::CONTENT_TYPE;
 use actix_web::web;
-use askama_actix::{Template, TemplateToResponse};
+use askama_actix::Template;
 use bytes::Bytes;
 
 #[derive(Template)]
@@ -27,24 +27,3 @@ async fn test_actix_web() {
     let bytes = response.body().await.unwrap();
     assert_eq!(bytes, Bytes::from_static("Hello, world!".as_ref()));
 }
-
-#[actix_rt::test]
-async fn test_actix_web_responder() {
-    let srv = actix_test::start(|| {
-        actix_web::App::new().service(web::resource("/").to(|| async {
-            let name = "world".to_owned();
-            HelloTemplate { name: &name }.to_response()
-        }))
-    });
-
-    let request = srv.get("/");
-    let mut response = request.send().await.unwrap();
-    assert!(response.status().is_success());
-    assert_eq!(
-        response.headers().get(CONTENT_TYPE).unwrap(),
-        "text/html; charset=utf-8"
-    );
-
-    let bytes = response.body().await.unwrap();
-    assert_eq!(bytes, Bytes::from_static("Hello, world!".as_ref()));
-}