Repository: djc/askama
Pull request #509 (2021-07-01 16:39:37)

Implement destructoring structs on "let" and "for" statement target 
Depends on #506.

Kijewski: Rebased on #506.

Patch:
diff --git a/askama_shared/src/generator.rs b/askama_shared/src/generator.rs
index a7f52ca..36d1841 100644
--- a/askama_shared/src/generator.rs
+++ b/askama_shared/src/generator.rs
@@ -824,9 +824,12 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
                     _ => false,
                 }
             }
-            Target::Tuple(targets) => targets
+            Target::Tuple(_, targets) => targets
                 .iter()
                 .any(|target| self.is_shadowing_variable(target)),
+            Target::Struct(_, named_targets) => named_targets
+                .iter()
+                .any(|(_, target)| self.is_shadowing_variable(target)),
         }
     }
 
@@ -1521,7 +1524,8 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
                 }
                 buf.write(name);
             }
-            Target::Tuple(targets) => {
+            Target::Tuple(path, targets) => {
+                buf.write(&path.join("::"));
                 buf.write("(");
                 for target in targets {
                     self.visit_target(buf, initialized, target);
@@ -1529,6 +1533,17 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
                 }
                 buf.write(")");
             }
+            Target::Struct(path, targets) => {
+                buf.write(&path.join("::"));
+                buf.write(" { ");
+                for (name, target) in targets {
+                    buf.write(normalize_identifier(name));
+                    buf.write(": ");
+                    self.visit_target(buf, initialized, target);
+                    buf.write(",");
+                }
+                buf.write(" }");
+            }
         }
     }
 
diff --git a/askama_shared/src/parser.rs b/askama_shared/src/parser.rs
index a1aabe7..261887b 100644
--- a/askama_shared/src/parser.rs
+++ b/askama_shared/src/parser.rs
@@ -4,7 +4,7 @@ use nom::character::complete::{anychar, char, digit1};
 use nom::combinator::{complete, map, opt, recognize, value};
 use nom::error::{Error, ParseError};
 use nom::multi::{fold_many0, many0, many1, separated_list0, separated_list1};
-use nom::sequence::{delimited, pair, preceded, tuple};
+use nom::sequence::{delimited, pair, preceded, terminated, tuple};
 use nom::{self, error_position, Compare, IResult, InputTake};
 use std::str;
 
@@ -138,7 +138,8 @@ pub struct Macro<'a> {
 #[derive(Debug, PartialEq)]
 pub enum Target<'a> {
     Name(&'a str),
-    Tuple(Vec<Target<'a>>),
+    Tuple(Vec<&'a str>, Vec<Target<'a>>),
+    Struct(Vec<&'a str>, Vec<(&'a str, Target<'a>)>),
 }
 
 #[derive(Clone, Copy, Debug, PartialEq)]
@@ -404,15 +405,21 @@ fn variant_path(i: &[u8]) -> IResult<&[u8], MatchVariant<'_>> {
     })(i)
 }
 
+fn named_target(i: &[u8]) -> IResult<&[u8], (&str, Target<'_>)> {
+    let (i, (src, target)) = pair(identifier, opt(preceded(ws(tag(":")), target)))(i)?;
+    Ok((i, (src, target.unwrap_or(Target::Name(src)))))
+}
+
 fn target(i: &[u8]) -> IResult<&[u8], Target<'_>> {
     let mut opt_opening_paren = map(opt(ws(tag("("))), |o| o.is_some());
     let mut opt_closing_paren = map(opt(ws(tag(")"))), |o| o.is_some());
 
+    // match tuples and unused parentheses
     let (i, target_is_tuple) = opt_opening_paren(i)?;
     if target_is_tuple {
         let (i, is_empty_tuple) = opt_closing_paren(i)?;
         if is_empty_tuple {
-            return Ok((i, Target::Tuple(vec![])));
+            return Ok((i, Target::Tuple(Vec::new(), Vec::new())));
         }
 
         let (i, first_target) = target(i)?;
@@ -429,10 +436,39 @@ fn target(i: &[u8]) -> IResult<&[u8], Target<'_>> {
             opt(ws(tag(","))),
             ws(tag(")")),
         ))(i)?;
-        Ok((i, Target::Tuple(targets)))
-    } else {
-        map(identifier, Target::Name)(i)
+        return Ok((i, Target::Tuple(Vec::new(), targets)));
+    }
+
+    // match structs
+    let (i, path) = opt(path)(i)?;
+    if let Some(path) = path {
+        let (i, is_unnamed_struct) = opt_opening_paren(i)?;
+        if is_unnamed_struct {
+            let (i, targets) = alt((
+                map(tag(")"), |_| Vec::new()),
+                terminated(
+                    separated_list1(ws(tag(",")), target),
+                    pair(opt(ws(tag(","))), ws(tag(")"))),
+                ),
+            ))(i)?;
+            return Ok((i, Target::Tuple(path, targets)));
+        } else {
+            let (i, targets) = preceded(
+                ws(tag("{")),
+                alt((
+                    map(tag("}"), |_| Vec::new()),
+                    terminated(
+                        separated_list1(ws(tag(",")), named_target),
+                        pair(opt(ws(tag(","))), ws(tag("}"))),
+                    ),
+                )),
+            )(i)?;
+            return Ok((i, Target::Struct(path, targets)));
+        }
     }
+
+    // neither nor struct
+    map(identifier, Target::Name)(i)
 }
 
 fn variant_name(i: &[u8]) -> IResult<&[u8], MatchVariant<'_>> {


Test patch:
diff --git a/testing/tests/let_destructoring.rs b/testing/tests/let_destructoring.rs
new file mode 100644
index 0000000..20f7fff
--- /dev/null
+++ b/testing/tests/let_destructoring.rs
@@ -0,0 +1,107 @@
+use askama::Template;
+
+#[derive(Template)]
+#[template(source = "{% let (a, b, c) = v %}{{a}}{{b}}{{c}}", ext = "txt")]
+struct LetDestructoringTuple {
+    v: (i32, i32, i32),
+}
+
+#[test]
+fn test_let_destruct_tuple() {
+    let t = LetDestructoringTuple { v: (1, 2, 3) };
+    assert_eq!(t.render().unwrap(), "123");
+}
+
+struct UnnamedStruct(i32, i32, i32);
+
+#[derive(Template)]
+#[template(
+    source = "{% let UnnamedStruct(a, b, c) = v %}{{a}}{{b}}{{c}}",
+    ext = "txt"
+)]
+struct LetDestructoringUnnamedStruct {
+    v: UnnamedStruct,
+}
+
+#[test]
+fn test_let_destruct_unnamed_struct() {
+    let t = LetDestructoringUnnamedStruct {
+        v: UnnamedStruct(1, 2, 3),
+    };
+    assert_eq!(t.render().unwrap(), "123");
+}
+
+#[derive(Template)]
+#[template(
+    source = "{% let UnnamedStruct(a, b, c) = v %}{{a}}{{b}}{{c}}",
+    ext = "txt"
+)]
+struct LetDestructoringUnnamedStructRef<'a> {
+    v: &'a UnnamedStruct,
+}
+
+#[test]
+fn test_let_destruct_unnamed_struct_ref() {
+    let v = UnnamedStruct(1, 2, 3);
+    let t = LetDestructoringUnnamedStructRef { v: &v };
+    assert_eq!(t.render().unwrap(), "123");
+}
+
+struct NamedStruct {
+    a: i32,
+    b: i32,
+    c: i32,
+}
+
+#[derive(Template)]
+#[template(
+    source = "{% let NamedStruct { a, b: d, c } = v %}{{a}}{{d}}{{c}}",
+    ext = "txt"
+)]
+struct LetDestructoringNamedStruct {
+    v: NamedStruct,
+}
+
+#[test]
+fn test_let_destruct_named_struct() {
+    let t = LetDestructoringNamedStruct {
+        v: NamedStruct { a: 1, b: 2, c: 3 },
+    };
+    assert_eq!(t.render().unwrap(), "123");
+}
+
+#[derive(Template)]
+#[template(
+    source = "{% let NamedStruct { a, b: d, c } = v %}{{a}}{{d}}{{c}}",
+    ext = "txt"
+)]
+struct LetDestructoringNamedStructRef<'a> {
+    v: &'a NamedStruct,
+}
+
+#[test]
+fn test_let_destruct_named_struct_ref() {
+    let v = NamedStruct { a: 1, b: 2, c: 3 };
+    let t = LetDestructoringNamedStructRef { v: &v };
+    assert_eq!(t.render().unwrap(), "123");
+}
+
+mod some {
+    pub mod path {
+        pub struct Struct<'a>(pub &'a str);
+    }
+}
+
+#[derive(Template)]
+#[template(source = "{% let some::path::Struct(v) = v %}{{v}}", ext = "txt")]
+struct LetDestructoringWithPath<'a> {
+    v: some::path::Struct<'a>,
+}
+
+#[test]
+fn test_let_destruct_with_path() {
+    let t = LetDestructoringWithPath {
+        v: some::path::Struct("hello"),
+    };
+    assert_eq!(t.render().unwrap(), "hello");
+}