Repository: djc/askama
Pull request #632 (2022-02-07 23:40:43)

Remove ext argument in integrations (breaking change)
Lest the diff gets forgotten! :)

This change was part of #594, but deferred until the next big release, because it breaks backward compatibility. 


Patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 9102b42..5438379 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -33,7 +33,7 @@ jobs:
       matrix:
         package: [
           askama, askama_derive, testing,
-          askama_actix, askama_axum, askama_escape, askama_gotham,
+          askama_actix, askama_axum, askama_escape, askama_gotham, askama_hyper,
           askama_mendes, askama_rocket, askama_tide, askama_warp,
         ]
     runs-on: ubuntu-latest
diff --git a/Cargo.toml b/Cargo.toml
index 13f45d8..bd8b8da 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,7 @@ members = [
     "askama_rocket",
     "askama_tide",
     "askama_warp",
+    "askama_hyper",
     "testing",
 ]
 
diff --git a/askama/Cargo.toml b/askama/Cargo.toml
index f421935..7a4aef1 100644
--- a/askama/Cargo.toml
+++ b/askama/Cargo.toml
@@ -27,6 +27,7 @@ urlencode = ["askama_derive/urlencode", "percent-encoding"]
 with-actix-web = ["askama_derive/with-actix-web"]
 with-axum = ["askama_derive/with-axum"]
 with-gotham = ["askama_derive/with-gotham"]
+with-hyper = ["askama_derive/with-hyper"]
 with-mendes = ["askama_derive/with-mendes"]
 with-rocket = ["askama_derive/with-rocket"]
 with-tide = ["askama_derive/with-tide"]
@@ -45,7 +46,7 @@ dep_num_traits = { package = "num-traits", version = "0.2.6", optional = true }
 percent-encoding = { version = "2.1.0", optional = true }
 serde = { version = "1.0", optional = true, features = ["derive"] }
 serde_json = { version = "1.0", optional = true }
-serde_yaml = { version = "0.8", optional = true }
+serde_yaml = { version = "0.9", optional = true }
 
 [package.metadata.docs.rs]
 features = ["config", "humansize", "num-traits", "serde-json", "serde-yaml"]
diff --git a/askama_actix/Cargo.toml b/askama_actix/Cargo.toml
index ea95615..075f78e 100644
--- a/askama_actix/Cargo.toml
+++ b/askama_actix/Cargo.toml
@@ -18,7 +18,7 @@ askama = { version = "0.11.2", path = "../askama", default-features = false, fea
 
 [dev-dependencies]
 actix-rt = { version = "2", default-features = false }
-actix-test = "=0.1.0-beta.13"
+actix-test = "=0.1.0"
 bytes = { version = "1" }
 
 [features]
diff --git a/askama_axum/src/lib.rs b/askama_axum/src/lib.rs
index da3d699..090d1d2 100644
--- a/askama_axum/src/lib.rs
+++ b/askama_axum/src/lib.rs
@@ -6,7 +6,7 @@ pub use askama::*;
 pub use axum_core::response::{IntoResponse, Response};
 use http::StatusCode;
 
-pub fn into_response<T: Template>(t: &T, _ext: &str) -> Response {
+pub fn into_response<T: Template>(t: &T) -> Response {
     match t.render() {
         Ok(body) => {
             let headers = [(
diff --git a/askama_derive/Cargo.toml b/askama_derive/Cargo.toml
index ce7abfa..1d176b7 100644
--- a/askama_derive/Cargo.toml
+++ b/askama_derive/Cargo.toml
@@ -23,6 +23,7 @@ num-traits = []
 with-actix-web = []
 with-axum = []
 with-gotham = []
+with-hyper = []
 with-mendes = []
 with-rocket = []
 with-tide = []
diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs
index 142ff7f..b57a6f6 100644
--- a/askama_derive/src/generator.rs
+++ b/askama_derive/src/generator.rs
@@ -7,7 +7,7 @@ use crate::CompileError;
 use proc_macro::TokenStream;
 use quote::{quote, ToTokens};
 
-use std::collections::HashMap;
+use std::collections::hash_map::{Entry, HashMap};
 use std::path::{Path, PathBuf};
 use std::{cmp, hash, mem, str};
 
@@ -293,11 +293,6 @@ impl<'a> Generator<'a> {
     // Takes a Context and generates the relevant implementations.
     fn build(mut self, ctx: &'a Context<'_>) -> Result<String, CompileError> {
         let mut buf = Buffer::new(0);
-        if !ctx.blocks.is_empty() {
-            if let Some(parent) = self.input.parent {
-                self.deref_to_parent(&mut buf, parent)?;
-            }
-        };
 
         self.impl_template(ctx, &mut buf)?;
         self.impl_display(&mut buf)?;
@@ -308,6 +303,8 @@ impl<'a> Generator<'a> {
         self.impl_axum_into_response(&mut buf)?;
         #[cfg(feature = "with-gotham")]
         self.impl_gotham_into_response(&mut buf)?;
+        #[cfg(feature = "with-hyper")]
+        self.impl_hyper_into_response(&mut buf)?;
         #[cfg(feature = "with-mendes")]
         self.impl_mendes_responder(&mut buf)?;
         #[cfg(feature = "with-rocket")]
@@ -376,24 +373,6 @@ impl<'a> Generator<'a> {
         Ok(())
     }
 
-    // Implement `Deref<Parent>` for an inheriting context struct.
-    fn deref_to_parent(
-        &mut self,
-        buf: &mut Buffer,
-        parent_type: &syn::Type,
-    ) -> Result<(), CompileError> {
-        self.write_header(buf, "::std::ops::Deref", None)?;
-        buf.writeln(&format!(
-            "type Target = {};",
-            parent_type.into_token_stream()
-        ))?;
-        buf.writeln("#[inline]")?;
-        buf.writeln("fn deref(&self) -> &Self::Target {")?;
-        buf.writeln("&self._parent")?;
-        buf.writeln("}")?;
-        buf.writeln("}")
-    }
-
     // Implement `Display` for the given context struct.
     fn impl_display(&mut self, buf: &mut Buffer) -> Result<(), CompileError> {
         self.write_header(buf, "::std::fmt::Display", None)?;
@@ -428,8 +407,7 @@ impl<'a> Generator<'a> {
             "fn into_response(self)\
              -> ::askama_axum::Response {",
         )?;
-        let ext = self.input.extension().unwrap_or("txt");
-        buf.writeln(&format!("::askama_axum::into_response(&self, {:?})", ext))?;
+        buf.writeln("::askama_axum::into_response(&self)")?;
         buf.writeln("}")?;
         buf.writeln("}")
     }
@@ -443,8 +421,32 @@ impl<'a> Generator<'a> {
             "fn into_response(self, _state: &::askama_gotham::State)\
              -> ::askama_gotham::Response<::askama_gotham::Body> {",
         )?;
+        buf.writeln("::askama_gotham::respond(&self)")?;
+        buf.writeln("}")?;
+        buf.writeln("}")
+    }
+
+    // Implement `From<Template> for hyper::Response<Body>`.
+    #[cfg(feature = "with-hyper")]
+    fn impl_hyper_into_response(&mut self, buf: &mut Buffer) -> Result<(), CompileError> {
+        let (impl_generics, orig_ty_generics, where_clause) =
+            self.input.ast.generics.split_for_impl();
+        let ident = &self.input.ast.ident;
+        buf.writeln(&format!(
+            "{} {{",
+            quote!(
+                impl #impl_generics ::core::convert::From<&#ident #orig_ty_generics>
+                for ::askama_hyper::hyper::Response<::askama_hyper::hyper::Body>
+                #where_clause
+            )
+        ))?;
+        buf.writeln("#[inline]")?;
+        buf.writeln(&format!(
+            "{} {{",
+            quote!(fn from(value: &#ident #orig_ty_generics) -> Self)
+        ))?;
         let ext = self.input.extension().unwrap_or("txt");
-        buf.writeln(&format!("::askama_gotham::respond(&self, {:?})", ext))?;
+        buf.writeln(&format!("::askama_hyper::respond(value, {:?})", ext))?;
         buf.writeln("}")?;
         buf.writeln("}")
     }
@@ -490,10 +492,7 @@ impl<'a> Generator<'a> {
              -> ::mendes::http::Response<A::ResponseBody> {",
         )?;
 
-        buf.writeln(&format!(
-            "::askama_mendes::into_response(app, req, &self, {:?})",
-            self.input.extension()
-        ))?;
+        buf.writeln("::askama_mendes::into_response(app, req, &self)")?;
         buf.writeln("}")?;
         buf.writeln("}")?;
         Ok(())
@@ -515,8 +514,7 @@ impl<'a> Generator<'a> {
             "fn respond_to(self, _: &::askama_rocket::Request) \
              -> ::askama_rocket::Result<'askama> {",
         )?;
-        let ext = self.input.extension().unwrap_or("txt");
-        buf.writeln(&format!("::askama_rocket::respond(&self, {:?})", ext))?;
+        buf.writeln("::askama_rocket::respond(&self)")?;
 
         buf.writeln("}")?;
         buf.writeln("}")?;
@@ -525,8 +523,6 @@ impl<'a> Generator<'a> {
 
     #[cfg(feature = "with-tide")]
     fn impl_tide_integrations(&mut self, buf: &mut Buffer) -> Result<(), CompileError> {
-        let ext = self.input.extension().unwrap_or("txt");
-
         self.write_header(
             buf,
             "::std::convert::TryInto<::askama_tide::tide::Body>",
@@ -537,7 +533,7 @@ impl<'a> Generator<'a> {
             #[inline]\n\
             fn try_into(self) -> ::askama_tide::askama::Result<::askama_tide::tide::Body> {",
         )?;
-        buf.writeln(&format!("::askama_tide::try_into_body(&self, {:?})", &ext))?;
+        buf.writeln("::askama_tide::try_into_body(&self)")?;
         buf.writeln("}")?;
         buf.writeln("}")?;
 
@@ -545,7 +541,7 @@ impl<'a> Generator<'a> {
         self.write_header(buf, "Into<::askama_tide::tide::Response>", None)?;
         buf.writeln("#[inline]")?;
         buf.writeln("fn into(self) -> ::askama_tide::tide::Response {")?;
-        buf.writeln(&format!("::askama_tide::into_response(&self, {:?})", ext))?;
+        buf.writeln("::askama_tide::into_response(&self)")?;
         buf.writeln("}\n}")
     }
 
@@ -554,8 +550,7 @@ impl<'a> Generator<'a> {
         self.write_header(buf, "::askama_warp::warp::reply::Reply", None)?;
         buf.writeln("#[inline]")?;
         buf.writeln("fn into_response(self) -> ::askama_warp::warp::reply::Response {")?;
-        let ext = self.input.extension().unwrap_or("txt");
-        buf.writeln(&format!("::askama_warp::reply(&self, {:?})", ext))?;
+        buf.writeln("::askama_warp::reply(&self)")?;
         buf.writeln("}")?;
         buf.writeln("}")
     }
@@ -1193,10 +1188,9 @@ impl<'a> Generator<'a> {
                         ),
                     };
 
-                    use std::collections::hash_map::Entry;
                     let id = match expr_cache.entry(expression.clone()) {
-                        Entry::Occupied(e) => *e.get(),
-                        Entry::Vacant(e) => {
+                        Entry::Occupied(e) if s.is_cachable() => *e.get(),
+                        e => {
                             let id = self.named;
                             self.named += 1;
 
@@ -1205,7 +1199,10 @@ impl<'a> Generator<'a> {
                             buf_expr.write(&expression);
                             buf_expr.writeln(",")?;
 
-                            e.insert(id);
+                            if let Entry::Vacant(e) = e {
+                                e.insert(id);
+                            }
+
                             id
                         }
                     };
diff --git a/askama_derive/src/input.rs b/askama_derive/src/input.rs
index c09f3d0..68d01db 100644
--- a/askama_derive/src/input.rs
+++ b/askama_derive/src/input.rs
@@ -16,15 +16,13 @@ pub(crate) struct TemplateInput<'a> {
     pub(crate) escaper: &'a str,
     pub(crate) ext: Option<String>,
     pub(crate) mime_type: String,
-    pub(crate) parent: Option<&'a syn::Type>,
     pub(crate) path: PathBuf,
 }
 
 impl TemplateInput<'_> {
     /// Extract the template metadata from the `DeriveInput` structure. This
     /// mostly recovers the data for the `TemplateInput` fields from the
-    /// `template()` attribute list fields; it also finds the of the `_parent`
-    /// field, if any.
+    /// `template()` attribute list fields.
     pub(crate) fn new<'n>(
         ast: &'n syn::DeriveInput,
         config: &'n Config<'_>,
@@ -51,27 +49,6 @@ impl TemplateInput<'_> {
             }
         };
 
-        // Check to see if a `_parent` field was defined on the context
-        // struct, and store the type for it for use in the code generator.
-        let parent = match ast.data {
-            syn::Data::Struct(syn::DataStruct {
-                fields: syn::Fields::Named(ref fields),
-                ..
-            }) => fields
-                .named
-                .iter()
-                .find(|f| f.ident.as_ref().filter(|name| *name == "_parent").is_some())
-                .map(|f| &f.ty),
-            _ => None,
-        };
-
-        if parent.is_some() {
-            eprint!(
-                "   --> in struct {}\n   = use of deprecated field '_parent'\n",
-                ast.ident
-            );
-        }
-
         // Validate syntax
         let syntax = syntax.map_or_else(
             || Ok(config.syntaxes.get(config.default_syntax).unwrap()),
@@ -117,7 +94,6 @@ impl TemplateInput<'_> {
             escaper,
             ext,
             mime_type,
-            parent,
             path,
         })
     }
diff --git a/askama_derive/src/parser.rs b/askama_derive/src/parser.rs
index efcad73..dc163fa 100644
--- a/askama_derive/src/parser.rs
+++ b/askama_derive/src/parser.rs
@@ -106,6 +106,39 @@ impl Expr<'_> {
             _ => false,
         }
     }
+
+    /// Returns `true` if the outcome of this expression may be used multiple times in the same
+    /// `write!()` call, without evaluating the expression again, i.e. the expression should be
+    /// side-effect free.
+    pub(crate) fn is_cachable(&self) -> bool {
+        match self {
+            // Literals are the definition of pure:
+            Expr::BoolLit(_) => true,
+            Expr::NumLit(_) => true,
+            Expr::StrLit(_) => true,
+            Expr::CharLit(_) => true,
+            // fmt::Display should have no effects:
+            Expr::Var(_) => true,
+            Expr::Path(_) => true,
+            // Check recursively:
+            Expr::Array(args) => args.iter().all(|arg| arg.is_cachable()),
+            Expr::Attr(lhs, _) => lhs.is_cachable(),
+            Expr::Index(lhs, rhs) => lhs.is_cachable() && rhs.is_cachable(),
+            Expr::Filter(_, args) => args.iter().all(|arg| arg.is_cachable()),
+            Expr::Unary(_, arg) => arg.is_cachable(),
+            Expr::BinOp(_, lhs, rhs) => lhs.is_cachable() && rhs.is_cachable(),
+            Expr::Range(_, lhs, rhs) => {
+                lhs.as_ref().map_or(true, |v| v.is_cachable())
+                    && rhs.as_ref().map_or(true, |v| v.is_cachable())
+            }
+            Expr::Group(arg) => arg.is_cachable(),
+            Expr::Tuple(args) => args.iter().all(|arg| arg.is_cachable()),
+            // We have too little information to tell if the expression is pure:
+            Expr::Call(_, _) => false,
+            Expr::RustMacro(_, _) => false,
+            Expr::Try(_) => false,
+        }
+    }
 }
 
 pub(crate) type When<'a> = (Ws, Target<'a>, Vec<Node<'a>>);
diff --git a/askama_gotham/src/lib.rs b/askama_gotham/src/lib.rs
index a32398d..bc699f6 100644
--- a/askama_gotham/src/lib.rs
+++ b/askama_gotham/src/lib.rs
@@ -9,7 +9,7 @@ use gotham::hyper::header;
 pub use gotham::hyper::{Body, Response, StatusCode};
 pub use gotham::state::State;
 
-pub fn respond<T: Template>(t: &T, _ext: &str) -> Response<Body> {
+pub fn respond<T: Template>(t: &T) -> Response<Body> {
     match t.render() {
         Ok(body) => Response::builder()
             .status(StatusCode::OK)
diff --git a/askama_hyper/Cargo.toml b/askama_hyper/Cargo.toml
new file mode 100644
index 0000000..6a7a213
--- /dev/null
+++ b/askama_hyper/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "askama_hyper"
+version = "0.1.0"
+description = "Hyper integration for Askama templates"
+documentation = "https://docs.rs/askama"
+keywords = ["markup", "template", "jinja2", "html"]
+categories = ["template-engine"]
+homepage = "https://github.com/djc/askama"
+repository = "https://github.com/djc/askama"
+license = "MIT OR Apache-2.0"
+workspace = ".."
+readme = "README.md"
+edition = "2018"
+
+[dependencies]
+askama = { version = "0.11.2", path = "../askama", default-features = false, features = ["with-hyper"] }
+hyper = { version = "0.14", default-features = false }
+
+[dev-dependencies]
+hyper = { version = "0.14", features = ["client"] }
+mime = "0.3"
+routerify = "3"
+tokio = { version = "1", default-features = false, features = ["net", "macros", "rt"] }
+
+[features]
+default = ["askama/default"]
+config = ["askama/config"]
+humansize = ["askama/humansize"]
+markdown = ["askama/markdown"]
+num-traits = ["askama/num-traits"]
+serde-json = ["askama/serde-json"]
+serde-yaml = ["askama/serde-yaml"]
+urlencode = ["askama/urlencode"]
diff --git a/askama_hyper/LICENSE-APACHE b/askama_hyper/LICENSE-APACHE
new file mode 120000
index 0000000..965b606
--- /dev/null
+++ b/askama_hyper/LICENSE-APACHE
@@ -0,0 +1,1 @@
+../LICENSE-APACHE
\ No newline at end of file
diff --git a/askama_hyper/LICENSE-MIT b/askama_hyper/LICENSE-MIT
new file mode 120000
index 0000000..76219eb
--- /dev/null
+++ b/askama_hyper/LICENSE-MIT
@@ -0,0 +1,1 @@
+../LICENSE-MIT
\ No newline at end of file
diff --git a/askama_hyper/README.md b/askama_hyper/README.md
new file mode 100644
index 0000000..024665a
--- /dev/null
+++ b/askama_hyper/README.md
@@ -0,0 +1,9 @@
+# askama_hyper: Askama integration with Hyper
+
+[![Documentation](https://docs.rs/askama_hyper/badge.svg)](https://docs.rs/askama_hyper/)
+[![Latest version](https://img.shields.io/crates/v/askama_hyper.svg)](https://crates.io/crates/askama_hyper)
+[![Build Status](https://github.com/djc/askama/workflows/CI/badge.svg)](https://github.com/djc/askama/actions?query=workflow%3ACI)
+[![Chat](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/djc/askama)
+
+Integration of the [Askama](https://github.com/djc/askama) templating engine in
+code building on the [hyper](https://crates.io/crates/hyper)-based web servers.
diff --git a/askama_hyper/src/lib.rs b/askama_hyper/src/lib.rs
new file mode 100644
index 0000000..07ede88
--- /dev/null
+++ b/askama_hyper/src/lib.rs
@@ -0,0 +1,29 @@
+#![forbid(unsafe_code)]
+#![deny(elided_lifetimes_in_paths)]
+#![deny(unreachable_pub)]
+
+pub use askama::*;
+#[doc(hidden)]
+pub use hyper;
+use hyper::{header, Body, Response, StatusCode};
+
+pub fn try_respond<T: Template>(t: &T, _ext: &str) -> Result<Response<Body>> {
+    Response::builder()
+        .status(StatusCode::OK)
+        .header(
+            header::CONTENT_TYPE,
+            header::HeaderValue::from_static(T::MIME_TYPE),
+        )
+        .body(t.render()?.into())
+        .map_err(|err| Error::Custom(Box::new(err)))
+}
+
+pub fn respond<T: Template>(t: &T, _ext: &str) -> Response<Body> {
+    match try_respond(t, _ext) {
+        Ok(response) => response,
+        Err(_) => Response::builder()
+            .status(StatusCode::INTERNAL_SERVER_ERROR)
+            .body(Body::empty())
+            .unwrap(),
+    }
+}
diff --git a/askama_hyper/templates/hello.html b/askama_hyper/templates/hello.html
new file mode 100644
index 0000000..8149be7
--- /dev/null
+++ b/askama_hyper/templates/hello.html
@@ -0,0 +1,1 @@
+Hello, {{ name }}!
diff --git a/askama_mendes/Cargo.toml b/askama_mendes/Cargo.toml
index efbff85..661acdd 100644
--- a/askama_mendes/Cargo.toml
+++ b/askama_mendes/Cargo.toml
@@ -14,7 +14,7 @@ edition = "2018"
 
 [dependencies]
 askama = { version = "0.11.2", path = "../askama", default-features = false, features = ["with-mendes", "mime", "mime_guess"] }
-mendes = "0.0.64"
+mendes = "0.1.0"
 
 [dev-dependencies]
 async-trait = "0.1.51"
diff --git a/askama_mendes/src/lib.rs b/askama_mendes/src/lib.rs
index 3bf3648..5eb3a3c 100644
--- a/askama_mendes/src/lib.rs
+++ b/askama_mendes/src/lib.rs
@@ -9,12 +9,7 @@ use mendes::http::Response;
 
 pub use askama::*;
 
-pub fn into_response<A, T>(
-    app: &A,
-    req: &Parts,
-    t: &T,
-    _ext: Option<&str>,
-) -> Response<A::ResponseBody>
+pub fn into_response<A, T>(app: &A, req: &Parts, t: &T) -> Response<A::ResponseBody>
 where
     A: Application,
     T: Template,
diff --git a/askama_rocket/src/lib.rs b/askama_rocket/src/lib.rs
index fb231a4..e2e84be 100644
--- a/askama_rocket/src/lib.rs
+++ b/askama_rocket/src/lib.rs
@@ -10,7 +10,7 @@ pub use rocket::request::Request;
 use rocket::response::Response;
 pub use rocket::response::{Responder, Result};
 
-pub fn respond<T: Template>(t: &T, _ext: &str) -> Result<'static> {
+pub fn respond<T: Template>(t: &T) -> Result<'static> {
     let rsp = t.render().map_err(|_| Status::InternalServerError)?;
     Response::build()
         .header(Header::new("content-type", T::MIME_TYPE))
diff --git a/askama_tide/src/lib.rs b/askama_tide/src/lib.rs
index be597bb..a7fa6c1 100644
--- a/askama_tide/src/lib.rs
+++ b/askama_tide/src/lib.rs
@@ -8,15 +8,15 @@ pub use tide;
 use askama::*;
 use tide::{Body, Response};
 
-pub fn try_into_body<T: Template>(t: &T, _ext: &str) -> Result<Body> {
+pub fn try_into_body<T: Template>(t: &T) -> Result<Body> {
     let string = t.render()?;
     let mut body = Body::from_string(string);
     body.set_mime(T::MIME_TYPE);
     Ok(body)
 }
 
-pub fn into_response<T: Template>(t: &T, ext: &str) -> Response {
-    match try_into_body(t, ext) {
+pub fn into_response<T: Template>(t: &T) -> Response {
+    match try_into_body(t) {
         Ok(body) => {
             let mut response = Response::new(200);
             response.set_body(body);
diff --git a/askama_warp/src/lib.rs b/askama_warp/src/lib.rs
index b2f0387..9c4bb9b 100644
--- a/askama_warp/src/lib.rs
+++ b/askama_warp/src/lib.rs
@@ -9,7 +9,7 @@ use warp::http::{self, header, StatusCode};
 use warp::hyper::Body;
 use warp::reply::Response;
 
-pub fn reply<T: askama::Template>(t: &T, _ext: &str) -> Response {
+pub fn reply<T: askama::Template>(t: &T) -> Response {
     match t.render() {
         Ok(body) => http::Response::builder()
             .status(StatusCode::OK)


Test patch:
diff --git a/askama_hyper/tests/basic.rs b/askama_hyper/tests/basic.rs
new file mode 100644
index 0000000..d0038c1
--- /dev/null
+++ b/askama_hyper/tests/basic.rs
@@ -0,0 +1,70 @@
+use std::convert::Infallible;
+
+use askama::Template;
+use hyper::body::to_bytes;
+use hyper::{Body, Client, Request, Response, Server};
+use routerify::ext::RequestExt;
+use routerify::{Router, RouterService};
+
+#[derive(Template)]
+#[template(path = "hello.html")]
+struct HelloTemplate<'a, N>
+where
+    N: std::fmt::Display,
+{
+    name: &'a N,
+}
+
+async fn hello_handler(req: Request<Body>) -> Result<Response<Body>, Infallible> {
+    let name = req.param("name").unwrap();
+    let template = &HelloTemplate { name: &name };
+    Ok(template.into())
+}
+
+fn router() -> Router<Body, Infallible> {
+    Router::builder()
+        .get("/hello/:name", hello_handler)
+        .build()
+        .unwrap()
+}
+
+#[tokio::test]
+async fn test_hyper() {
+    let addr = ([127, 0, 0, 1], 0).into();
+    let service = RouterService::new(router()).expect("Could not create service");
+    let server = Server::bind(&addr).serve(service);
+    let local_addr = server.local_addr();
+
+    let (tx, rx) = tokio::sync::oneshot::channel::<()>();
+    let serve = async move {
+        let server = server.with_graceful_shutdown(async {
+            rx.await.expect("Could not await signal to stop");
+        });
+        server.await.expect("Could not serve");
+    };
+    let query = async move {
+        let uri = format!("http://{}/hello/world", local_addr)
+            .parse()
+            .expect("Could not format URI");
+        let client = Client::new();
+
+        let res = client.get(uri).await.expect("Could not query client");
+        assert_eq!(res.status(), hyper::StatusCode::OK);
+
+        let content_type = res
+            .headers()
+            .get("content-type")
+            .expect("Response did not contain content-type header")
+            .to_str()
+            .expect("Content-type was not a UTF-8 string");
+        assert_eq!(content_type, mime::TEXT_HTML_UTF_8.to_string());
+
+        let body = to_bytes(res).await.expect("No body returned");
+        let body = std::str::from_utf8(&body).expect("Body was not UTF-8");
+        assert_eq!(body, "Hello, world!");
+
+        tx.send(()).unwrap();
+    };
+
+    tokio::join!(serve, query);
+}
diff --git a/testing/tests/inheritance.rs b/testing/tests/inheritance.rs
index 2dac362..5d8aefb 100644
--- a/testing/tests/inheritance.rs
+++ b/testing/tests/inheritance.rs
@@ -1,3 +1,5 @@
+use std::ops::Deref;
+
 use askama::Template;
 
 #[derive(Template)]
@@ -9,7 +11,15 @@ struct BaseTemplate<'a> {
 #[derive(Template)]
 #[template(path = "child.html")]
 struct ChildTemplate<'a> {
-    _parent: BaseTemplate<'a>,
+    _parent: &'a BaseTemplate<'a>,
+}
+
+impl<'a> Deref for ChildTemplate<'a> {
+    type Target = BaseTemplate<'a>;
+
+    fn deref(&self) -> &Self::Target {
+        self._parent
+    }
 }
 
 #[test]
@@ -21,7 +31,7 @@ fn test_use_base_directly() {
 #[test]
 fn test_simple_extends() {
     let t = ChildTemplate {
-        _parent: BaseTemplate { title: "Bar" },
+        _parent: &BaseTemplate { title: "Bar" },
     };
     assert_eq!(
         t.render().unwrap(),
@@ -43,6 +53,7 @@ fn test_empty_child() {
 
 pub mod parent {
     use askama::Template;
+
     #[derive(Template)]
     #[template(path = "base.html")]
     pub struct BaseTemplate<'a> {
@@ -53,17 +64,26 @@ pub mod parent {
 pub mod child {
     use super::parent::*;
     use askama::Template;
+
     #[derive(Template)]
     #[template(path = "child.html")]
     pub struct ChildTemplate<'a> {
-        pub _parent: BaseTemplate<'a>,
+        pub _parent: &'a BaseTemplate<'a>,
+    }
+
+    impl<'a> std::ops::Deref for ChildTemplate<'a> {
+        type Target = BaseTemplate<'a>;
+
+        fn deref(&self) -> &Self::Target {
+            self._parent
+        }
     }
 }
 
 #[test]
 fn test_different_module() {
     let t = child::ChildTemplate {
-        _parent: parent::BaseTemplate { title: "a" },
+        _parent: &parent::BaseTemplate { title: "a" },
     };
     assert_eq!(
         t.render().unwrap(),
@@ -81,6 +101,14 @@ struct NestedChildTemplate {
     _parent: NestedBaseTemplate,
 }
 
+impl Deref for NestedChildTemplate {
+    type Target = NestedBaseTemplate;
+
+    fn deref(&self) -> &Self::Target {
+        &self._parent
+    }
+}
+
 #[test]
 fn test_nested_blocks() {
     let t = NestedChildTemplate {
@@ -109,6 +137,14 @@ struct DeepKidTemplate {
     item: String,
 }
 
+impl Deref for DeepKidTemplate {
+    type Target = DeepMidTemplate;
+
+    fn deref(&self) -> &Self::Target {
+        &self._parent
+    }
+}
+
 #[test]
 fn test_deep() {
     let t = DeepKidTemplate {