Repository: djc/askama
Pull request #608 (2022-01-14 14:40:53)

Parse tuple expressions
Askama understands how to destructure tuples in let and match
statements, but it does not understand how to build a tuple.

This PR fixes this shortcoming.

Kijewski: I think this would resolve #607 quite nicely. It would compile the example to ↓, which is reasonably similar to what you would write by hand:
```rs
match &(
    self.client.can_post(&(self.post)),
    self.client.can_update(&(self.post)),
) {
    (false, false) => {
        writer.write_str("No!")?;
    }
    (can_post, can_update) => {
        writer.write_str("<ul>")?;
        if *(&(can_post) as &bool) {
            writer.write_str("<li>post</li>")?;
        }
        if *(&(can_update) as &bool) {
            writer.write_str("<li>update</li>")?;
        }
        writer.write_str("</ul>")?;
    }
}
```
Kijewski: You're welcome :)

Patch:
diff --git a/askama_shared/src/generator.rs b/askama_shared/src/generator.rs
index de2fdcb..7080b87 100644
--- a/askama_shared/src/generator.rs
+++ b/askama_shared/src/generator.rs
@@ -1061,6 +1061,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
             Expr::Call(ref obj, ref args) => self.visit_call(buf, obj, args)?,
             Expr::RustMacro(name, args) => self.visit_rust_macro(buf, name, args),
             Expr::Try(ref expr) => self.visit_try(buf, expr.as_ref())?,
+            Expr::Tuple(ref exprs) => self.visit_tuple(buf, exprs)?,
         })
     }
 
@@ -1403,6 +1404,23 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
         Ok(DisplayWrap::Unwrapped)
     }
 
+    fn visit_tuple(
+        &mut self,
+        buf: &mut Buffer,
+        exprs: &[Expr<'_>],
+    ) -> Result<DisplayWrap, CompileError> {
+        buf.write("(");
+        for (index, expr) in exprs.iter().enumerate() {
+            if index > 0 {
+                buf.write(" ");
+            }
+            self.visit_expr(buf, expr)?;
+            buf.write(",");
+        }
+        buf.write(")");
+        Ok(DisplayWrap::Unwrapped)
+    }
+
     fn visit_array(
         &mut self,
         buf: &mut Buffer,
diff --git a/askama_shared/src/parser.rs b/askama_shared/src/parser.rs
index 63cea0c..085d73b 100644
--- a/askama_shared/src/parser.rs
+++ b/askama_shared/src/parser.rs
@@ -61,6 +61,7 @@ pub enum Expr<'a> {
     BinOp(&'a str, Box<Expr<'a>>, Box<Expr<'a>>),
     Range(&'a str, Option<Box<Expr<'a>>>, Option<Box<Expr<'a>>>),
     Group(Box<Expr<'a>>),
+    Tuple(Vec<Expr<'a>>),
     Call(Box<Expr<'a>>, Vec<Expr<'a>>),
     RustMacro(&'a str, &'a str),
     Try(Box<Expr<'a>>),
@@ -488,9 +489,31 @@ fn parameters(i: &str) -> IResult<&str, Vec<&str>> {
 }
 
 fn expr_group(i: &str) -> IResult<&str, Expr<'_>> {
-    map(delimited(ws(char('(')), expr_any, ws(char(')'))), |s| {
-        Expr::Group(Box::new(s))
-    })(i)
+    let (i, expr) = preceded(ws(char('(')), opt(expr_any))(i)?;
+    let expr = match expr {
+        Some(expr) => expr,
+        None => {
+            let (i, _) = char(')')(i)?;
+            return Ok((i, Expr::Tuple(vec![])));
+        }
+    };
+
+    let (i, comma) = ws(opt(peek(char(','))))(i)?;
+    if comma.is_none() {
+        let (i, _) = char(')')(i)?;
+        return Ok((i, Expr::Group(Box::new(expr))));
+    }
+
+    let mut exprs = vec![expr];
+    let (i, _) = fold_many0(
+        preceded(char(','), ws(expr_any)),
+        || (),
+        |_, expr| {
+            exprs.push(expr);
+        },
+    )(i)?;
+    let (i, _) = pair(ws(opt(char(','))), char(')'))(i)?;
+    Ok((i, Expr::Tuple(exprs)))
 }
 
 fn expr_single(i: &str) -> IResult<&str, Expr<'_>> {
@@ -1687,4 +1710,123 @@ mod tests {
             vec![Node::Comment(Ws(false, false))],
         );
     }
+
+    #[test]
+    fn test_parse_tuple() {
+        use super::Expr::*;
+        let syntax = Syntax::default();
+        assert_eq!(
+            super::parse("{{ () }}", &syntax).unwrap(),
+            vec![Node::Expr(Ws(false, false), Tuple(vec![]),)],
+        );
+        assert_eq!(
+            super::parse("{{ (1) }}", &syntax).unwrap(),
+            vec![Node::Expr(Ws(false, false), Group(Box::new(NumLit("1"))),)],
+        );
+        assert_eq!(
+            super::parse("{{ (1,) }}", &syntax).unwrap(),
+            vec![Node::Expr(Ws(false, false), Tuple(vec![NumLit("1")]),)],
+        );
+        assert_eq!(
+            super::parse("{{ (1, ) }}", &syntax).unwrap(),
+            vec![Node::Expr(Ws(false, false), Tuple(vec![NumLit("1")]),)],
+        );
+        assert_eq!(
+            super::parse("{{ (1 ,) }}", &syntax).unwrap(),
+            vec![Node::Expr(Ws(false, false), Tuple(vec![NumLit("1")]),)],
+        );
+        assert_eq!(
+            super::parse("{{ (1 , ) }}", &syntax).unwrap(),
+            vec![Node::Expr(Ws(false, false), Tuple(vec![NumLit("1")]),)],
+        );
+        assert_eq!(
+            super::parse("{{ (1, 2) }}", &syntax).unwrap(),
+            vec![Node::Expr(
+                Ws(false, false),
+                Tuple(vec![NumLit("1"), NumLit("2")]),
+            )],
+        );
+        assert_eq!(
+            super::parse("{{ (1, 2,) }}", &syntax).unwrap(),
+            vec![Node::Expr(
+                Ws(false, false),
+                Tuple(vec![NumLit("1"), NumLit("2")]),
+            )],
+        );
+        assert_eq!(
+            super::parse("{{ (1, 2, 3) }}", &syntax).unwrap(),
+            vec![Node::Expr(
+                Ws(false, false),
+                Tuple(vec![NumLit("1"), NumLit("2"), NumLit("3")]),
+            )],
+        );
+        assert_eq!(
+            super::parse("{{ ()|abs }}", &syntax).unwrap(),
+            vec![Node::Expr(
+                Ws(false, false),
+                Filter("abs", vec![Tuple(vec![])]),
+            )],
+        );
+        assert_eq!(
+            super::parse("{{ () | abs }}", &syntax).unwrap(),
+            vec![Node::Expr(
+                Ws(false, false),
+                BinOp("|", Box::new(Tuple(vec![])), Box::new(Var("abs"))),
+            )],
+        );
+        assert_eq!(
+            super::parse("{{ (1)|abs }}", &syntax).unwrap(),
+            vec![Node::Expr(
+                Ws(false, false),
+                Filter("abs", vec![Group(Box::new(NumLit("1")))]),
+            )],
+        );
+        assert_eq!(
+            super::parse("{{ (1) | abs }}", &syntax).unwrap(),
+            vec![Node::Expr(
+                Ws(false, false),
+                BinOp(
+                    "|",
+                    Box::new(Group(Box::new(NumLit("1")))),
+                    Box::new(Var("abs"))
+                ),
+            )],
+        );
+        assert_eq!(
+            super::parse("{{ (1,)|abs }}", &syntax).unwrap(),
+            vec![Node::Expr(
+                Ws(false, false),
+                Filter("abs", vec![Tuple(vec![NumLit("1")])]),
+            )],
+        );
+        assert_eq!(
+            super::parse("{{ (1,) | abs }}", &syntax).unwrap(),
+            vec![Node::Expr(
+                Ws(false, false),
+                BinOp(
+                    "|",
+                    Box::new(Tuple(vec![NumLit("1")])),
+                    Box::new(Var("abs"))
+                ),
+            )],
+        );
+        assert_eq!(
+            super::parse("{{ (1, 2)|abs }}", &syntax).unwrap(),
+            vec![Node::Expr(
+                Ws(false, false),
+                Filter("abs", vec![Tuple(vec![NumLit("1"), NumLit("2")])]),
+            )],
+        );
+        assert_eq!(
+            super::parse("{{ (1, 2) | abs }}", &syntax).unwrap(),
+            vec![Node::Expr(
+                Ws(false, false),
+                BinOp(
+                    "|",
+                    Box::new(Tuple(vec![NumLit("1"), NumLit("2")])),
+                    Box::new(Var("abs"))
+                ),
+            )],
+        );
+    }
 }


Test patch:
diff --git a/testing/tests/tuple.rs b/testing/tests/tuple.rs
new file mode 100644
index 0000000..37d313c
--- /dev/null
+++ b/testing/tests/tuple.rs
@@ -0,0 +1,82 @@
+use askama::Template;
+
+struct Post {
+    id: u32,
+}
+
+struct Client<'a> {
+    can_post_ids: &'a [u32],
+    can_update_ids: &'a [u32],
+}
+
+impl Client<'_> {
+    fn can_post(&self, post: &Post) -> bool {
+        self.can_post_ids.contains(&post.id)
+    }
+
+    fn can_update(&self, post: &Post) -> bool {
+        self.can_update_ids.contains(&post.id)
+    }
+}
+
+#[derive(Template)]
+#[template(
+    source = r#"
+{%- match (client.can_post(post), client.can_update(post)) -%}
+    {%- when (false, false) -%}
+        No!
+    {%- when (can_post, can_update) -%}
+        <ul>
+        {%- if can_post -%}<li>post</li>{%- endif -%}
+        {%- if can_update -%}<li>update</li>{%- endif -%}
+        </ul>
+{%- endmatch -%}
+"#,
+    ext = "txt"
+)]
+struct TupleTemplate<'a> {
+    client: &'a Client<'a>,
+    post: &'a Post,
+}
+
+#[test]
+fn test_tuple() {
+    let template = TupleTemplate {
+        client: &Client {
+            can_post_ids: &[1, 2],
+            can_update_ids: &[2, 3],
+        },
+        post: &Post { id: 1 },
+    };
+    assert_eq!(template.render().unwrap(), "<ul><li>post</li></ul>");
+
+    let template = TupleTemplate {
+        client: &Client {
+            can_post_ids: &[1, 2],
+            can_update_ids: &[2, 3],
+        },
+        post: &Post { id: 2 },
+    };
+    assert_eq!(
+        template.render().unwrap(),
+        "<ul><li>post</li><li>update</li></ul>"
+    );
+
+    let template = TupleTemplate {
+        client: &Client {
+            can_post_ids: &[1, 2],
+            can_update_ids: &[2, 3],
+        },
+        post: &Post { id: 3 },
+    };
+    assert_eq!(template.render().unwrap(), "<ul><li>update</li></ul>");
+
+    let template = TupleTemplate {
+        client: &Client {
+            can_post_ids: &[1, 2],
+            can_update_ids: &[2, 3],
+        },
+        post: &Post { id: 4 },
+    };
+    assert_eq!(template.render().unwrap(), "No!");
+}