Repository: djc/askama
Pull request #553 (2021-11-10 07:17:44)

Prepare for actix-web v4

djc: This looks good to me in principle, can you fix up the failing CI workflow?

Patch:
diff --git a/askama_actix/Cargo.toml b/askama_actix/Cargo.toml
index 253fbd0..8514a9b 100644
--- a/askama_actix/Cargo.toml
+++ b/askama_actix/Cargo.toml
@@ -14,10 +14,11 @@ readme = "README.md"
 edition = "2018"
 
 [dependencies]
-actix-web = { version = "3", default-features = false }
+actix-web = { version = "4.0.0-beta.10", default-features = false }
 askama = { version = "0.11.0-beta.1", path = "../askama", default-features = false, features = ["with-actix-web", "mime", "mime_guess"] }
-bytes = { version = "0.5" }
+bytes = { version = "1" }
 futures-util = { version = "0.3" }
 
 [dev-dependencies]
-actix-rt = { version = "1", default-features = false }
+actix-rt = { version = "2", default-features = false }
+actix-test = "0.1.0-beta.5"
diff --git a/askama_actix/src/lib.rs b/askama_actix/src/lib.rs
index bb58174..2496d92 100644
--- a/askama_actix/src/lib.rs
+++ b/askama_actix/src/lib.rs
@@ -3,23 +3,24 @@
 pub use askama::*;
 use bytes::BytesMut;
 
-use actix_web::{error::ErrorInternalServerError, Error, HttpResponse};
+use actix_web::{error::ErrorInternalServerError, HttpResponse};
 
 pub trait TemplateToResponse {
-    fn to_response(&self) -> ::std::result::Result<HttpResponse, Error>;
+    fn to_response(&self) -> HttpResponse;
 }
 
 impl<T: askama::Template> TemplateToResponse for T {
-    fn to_response(&self) -> ::std::result::Result<HttpResponse, Error> {
+    fn to_response(&self) -> HttpResponse {
         let mut buffer = BytesMut::with_capacity(self.size_hint());
-        self.render_into(&mut buffer)
-            .map_err(|_| ErrorInternalServerError("Template parsing error"))?;
+        if self.render_into(&mut buffer).is_err() {
+            return ErrorInternalServerError("Template parsing error").error_response();
+        }
 
         let ctype =
             askama::mime::extension_to_mime_type(self.extension().unwrap_or("txt")).to_string();
-        Ok(HttpResponse::Ok()
+        HttpResponse::Ok()
             .content_type(ctype.as_str())
-            .body(buffer.freeze()))
+            .body(buffer.freeze())
     }
 }
 
diff --git a/askama_shared/src/generator.rs b/askama_shared/src/generator.rs
index f6d7cca..6c1b151 100644
--- a/askama_shared/src/generator.rs
+++ b/askama_shared/src/generator.rs
@@ -210,15 +210,13 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
     // Implement Actix-web's `Responder`.
     fn impl_actix_web_responder(&mut self, buf: &mut Buffer) -> Result<(), CompileError> {
         self.write_header(buf, "::actix_web::Responder", None)?;
-        buf.writeln("type Future = ::askama_actix::futures::Ready<::std::result::Result<::actix_web::HttpResponse, Self::Error>>;")?;
-        buf.writeln("type Error = ::actix_web::Error;")?;
         buf.writeln(
             "fn respond_to(self, _req: &::actix_web::HttpRequest) \
-             -> Self::Future {",
+             -> ::actix_web::HttpResponse {",
         )?;
 
         buf.writeln("use ::askama_actix::TemplateToResponse;")?;
-        buf.writeln("::askama_actix::futures::ready(self.to_response())")?;
+        buf.writeln("self.to_response()")?;
 
         buf.writeln("}")?;
         buf.writeln("}")
@@ -1278,15 +1276,38 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
         method: &str,
         args: &[Expr<'_>],
     ) -> Result<DisplayWrap, CompileError> {
-        if let Expr::Var("self") = obj {
-            buf.write("self");
+        if matches!(obj, Expr::Var("loop")) {
+            match method {
+                "cycle" => match args {
+                    [arg] => {
+                        if matches!(arg, Expr::Array(arr) if arr.is_empty()) {
+                            panic!("loop.cycle(…) cannot use an empty array.");
+                        }
+                        buf.write("({");
+                        buf.write("let _cycle = &(");
+                        self.visit_expr(buf, arg)?;
+                        buf.writeln(");")?;
+                        buf.writeln("let _len = _cycle.len();")?;
+                        buf.writeln("if _len == 0 {")?;
+                        buf.writeln("return ::core::result::Result::Err(::askama::Error::Fmt(::core::fmt::Error));")?;
+                        buf.writeln("}")?;
+                        buf.writeln("_cycle[_loop_item.index % _len]")?;
+                        buf.writeln("})")?;
+                    }
+                    _ => return Err("loop.cycle(…) expects exactly one argument".into()),
+                },
+                s => return Err(format!("unknown loop method: {:?}", s).into()),
+            }
         } else {
-            self.visit_expr(buf, obj)?;
+            if let Expr::Var("self") = obj {
+                buf.write("self");
+            } else {
+                self.visit_expr(buf, obj)?;
+            }
+            buf.write(&format!(".{}(", normalize_identifier(method)));
+            self._visit_args(buf, args)?;
+            buf.write(")");
         }
-
-        buf.write(&format!(".{}(", normalize_identifier(method)));
-        self._visit_args(buf, args)?;
-        buf.write(")");
         Ok(DisplayWrap::Unwrapped)
     }
 
diff --git a/askama_shared/src/input.rs b/askama_shared/src/input.rs
index c94c217..30340b5 100644
--- a/askama_shared/src/input.rs
+++ b/askama_shared/src/input.rs
@@ -31,15 +31,11 @@ impl<'a> TemplateInput<'a> {
         let meta = ast
             .attrs
             .iter()
-            .find_map(|attr| match attr.parse_meta() {
-                Ok(m) => {
-                    if m.path().is_ident("template") {
-                        Some(Ok(m))
-                    } else {
-                        None
-                    }
-                }
-                Err(e) => Some(Err(format!("unable to parse attribute: {}", e).into())),
+            .find_map(|attr| {
+                attr.path.is_ident("template").then(|| {
+                    attr.parse_meta()
+                        .map_err(|e| format!("unable to parse attribute: {}", e).into())
+                })
             })
             .unwrap_or(Err(CompileError::Static("no attribute 'template' found")))?;
 


Test patch:
diff --git a/askama_actix/tests/basic.rs b/askama_actix/tests/basic.rs
index 125270f..070cbf6 100644
--- a/askama_actix/tests/basic.rs
+++ b/askama_actix/tests/basic.rs
@@ -1,5 +1,4 @@
 use actix_web::http::header::CONTENT_TYPE;
-use actix_web::test;
 use actix_web::web;
 use askama_actix::{Template, TemplateToResponse};
 use bytes::Bytes;
@@ -12,7 +11,7 @@ struct HelloTemplate<'a> {
 
 #[actix_rt::test]
 async fn test_actix_web() {
-    let srv = test::start(|| {
+    let srv = actix_test::start(|| {
         actix_web::App::new()
             .service(web::resource("/").to(|| async { HelloTemplate { name: "world" } }))
     });
@@ -31,10 +30,10 @@ async fn test_actix_web() {
 
 #[actix_rt::test]
 async fn test_actix_web_responder() {
-    let srv = test::start(|| {
+    let srv = actix_test::start(|| {
         actix_web::App::new().service(web::resource("/").to(|| async {
             let name = "world".to_owned();
-            HelloTemplate { name: &name }.to_response()
+            HelloTemplate { name: &name }.to_response().await
         }))
     });
 
diff --git a/testing/tests/loops.rs b/testing/tests/loops.rs
index 49c37e0..f014068 100644
--- a/testing/tests/loops.rs
+++ b/testing/tests/loops.rs
@@ -372,3 +372,48 @@ fn test_loop_break_continue() {
     };
     assert_eq!(t.render().unwrap(), "x1yx2yx3yx11x4yx5y");
 }
+
+#[derive(Template)]
+#[template(
+    source = r#"{% for v in values %}{{loop.cycle(["r", "g", "b"])}}{{v}},{% endfor %}"#,
+    ext = "txt"
+)]
+struct ForCycle<'a> {
+    values: &'a [u8],
+}
+
+#[test]
+fn test_for_cycle() {
+    let t = ForCycle {
+        values: &[1, 2, 3, 4, 5, 6, 7, 8, 9],
+    };
+    assert_eq!(t.render().unwrap(), "r1,g2,b3,r4,g5,b6,r7,g8,b9,");
+}
+
+#[derive(Template)]
+#[template(
+    source = r#"{% for v in values %}{{loop.cycle(cycle)}}{{v}},{% endfor %}"#,
+    ext = "txt"
+)]
+struct ForCycleDynamic<'a> {
+    values: &'a [u8],
+    cycle: &'a [char],
+}
+
+#[test]
+fn test_for_cycle_dynamic() {
+    let t = ForCycleDynamic {
+        values: &[1, 2, 3, 4, 5, 6, 7, 8, 9],
+        cycle: &['a', 'b', 'c', 'd'],
+    };
+    assert_eq!(t.render().unwrap(), "a1,b2,c3,d4,a5,b6,c7,d8,a9,");
+}
+
+#[test]
+fn test_for_cycle_empty() {
+    let t = ForCycleDynamic {
+        values: &[1, 2, 3, 4, 5, 6, 7, 8, 9],
+        cycle: &[],
+    };
+    assert!(t.render().is_err())
+}
diff --git a/testing/tests/ui/loop_cycle_wrong_argument_count.rs b/testing/tests/ui/loop_cycle_wrong_argument_count.rs
new file mode 100644
index 0000000..e87f4f4
--- /dev/null
+++ b/testing/tests/ui/loop_cycle_wrong_argument_count.rs
@@ -0,0 +1,13 @@
+use askama::Template;
+
+#[derive(Template)]
+#[template(
+    source = r#"{% for v in values %}{{ loop.cycle("r", "g", "b") }}{{ v }},{% endfor %}"#,
+    ext = "txt"
+)]
+struct ForCycle<'a> {
+    values: &'a [u8],
+}
+
+fn main() {
+}
diff --git a/testing/tests/ui/loop_cycle_wrong_argument_count.stderr b/testing/tests/ui/loop_cycle_wrong_argument_count.stderr
new file mode 100644
index 0000000..acf3bb1
--- /dev/null
+++ b/testing/tests/ui/loop_cycle_wrong_argument_count.stderr
@@ -0,0 +1,7 @@
+error: loop.cycle(…) expects exactly one argument
+ --> $DIR/loop_cycle_wrong_argument_count.rs:3:10
+  |
+3 | #[derive(Template)]
+  |          ^^^^^^^^
+  |
+  = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)