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

Issue:
Feature request: enhance match to include multiple targets
This has previously been discussed at https://github.com/djc/askama/issues/711#issuecomment-1794340598 but I would like to move it to its own issue as it can be implemented separately.

The request is to enhance Askana's syntax for the `match` block (see https://djc.github.io/askama/template_syntax.html#match) to handle multiple targets in the same 'arm'.

See https://doc.rust-lang.org/reference/expressions/match-expr.html for the Rust syntax in the Reference Manual. Note that a match arm can have multiple targets; the object of this feature request is to replicate this in Askama. (I am not requesting 'match arm guards' (`if` clauses)).

So given code like
```
enum Suit {Clubs, Diamonds, Hearts, Spades}
```
one could write a template like 
```
The card is
{% match suit %}
   {% when Suit::Clubs or Suit::Spades %}
     black
   {% when Suit::Diamonds or Suit::Hearts %}
     red
{% endmatch %}
```
Alternatively, `or` could be replaced by `|` in the proposed design, but I think Askama prefers keywords to symbols.

Looking at the code, `combinator/mod.rs(587)`*ff* defines how match statements are stored in the parse structure and how the code is parsed into this structure, and line 232*ff* covers `when` arms. So the `target` field would need to be a vector of `Targets`, and the `when()` function would need to be able to parse the modified `when` syntax., and the `parse()` function for `Match` around 595 also would need changing.

The `Match` structure is used in `generator.rs` around 544*ff* to generate the Rust code. This currently calls `visit_target` at 1559*ff* to generate the actual target; this would need to be changed around 576 to a call for each target and to output intervening `|` tokens.

There is some code around `heritage.rs(93)`. I'm not sure what it does, but it looks like it moves the match arms without change, so maybe it does not require any change.

Also, the documentation will need changing, of course. near `template_syntax.md(417)`*ff*.

The test could look something like the following (in `testing/tests/matches.rs)`, with the template above in `testing/templates/match-enum.html`).

```
enum Suit {Clubs, Diamonds, Hearts, Spades}
#[derive(Template)]
#[template(path = "match-enum.html")]
struct MatchEnumTemplate {
    item: Suit,
}

#[test]
fn test_match_enum() {
    let s = MatchEnumTemplate { item: Suit::Clubs};
    assert_eq!(s.render().unwrap(), "\nThe card is black\n");

    let s = MatchEnumTemplate { item: Suit::Hearts};
    assert_eq!(s.render().unwrap(), "\nThe card is red\n");

}
```


Discussion / hints:
I'm happy to review a PR for this as proposed.

Fix patch:
diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs
index eb293b0ac..3e4801f4f 100644
--- a/askama_derive/src/generator.rs
+++ b/askama_derive/src/generator.rs
@@ -1575,6 +1575,16 @@ impl<'a> Generator<'a> {
                 }
                 buf.write(name);
             }
+            Target::OrChain(targets) => match targets.first() {
+                None => buf.write("_"),
+                Some(first_target) => {
+                    self.visit_target(buf, initialized, first_level, first_target);
+                    for target in &targets[1..] {
+                        buf.write(" | ");
+                        self.visit_target(buf, initialized, first_level, target);
+                    }
+                }
+            },
             Target::Tuple(path, targets) => {
                 buf.write(&path.join("::"));
                 buf.write("(");
diff --git a/askama_parser/src/node.rs b/askama_parser/src/node.rs
index 4da174252..ba117c57d 100644
--- a/askama_parser/src/node.rs
+++ b/askama_parser/src/node.rs
@@ -128,10 +128,23 @@ pub enum Target<'a> {
     CharLit(&'a str),
     BoolLit(&'a str),
     Path(Vec<&'a str>),
+    OrChain(Vec<Target<'a>>),
 }
 
 impl<'a> Target<'a> {
+    /// Parses multiple targets with `or` separating them
     pub(super) fn parse(i: &'a str) -> ParseResult<'a, Self> {
+        map(
+            separated_list1(ws(tag("or")), Self::parse_one),
+            |mut opts| match opts.len() {
+                1 => opts.pop().unwrap(),
+                _ => Self::OrChain(opts),
+            },
+        )(i)
+    }
+
+    /// Parses a single target without an `or`, unless it is wrapped in parentheses.
+    fn parse_one(i: &'a str) -> ParseResult<'a, Self> {
         let mut opt_opening_paren = map(opt(ws(char('('))), |o| o.is_some());
         let mut opt_closing_paren = map(opt(ws(char(')'))), |o| o.is_some());
         let mut opt_opening_brace = map(opt(ws(char('{'))), |o| o.is_some());


Test patch:
diff --git a/testing/templates/match-enum-or.html b/testing/templates/match-enum-or.html
new file mode 100644
index 000000000..ffd364db2
--- /dev/null
+++ b/testing/templates/match-enum-or.html
@@ -0,0 +1,8 @@
+The card is
+{%- match suit %}
+   {%- when Suit::Clubs or Suit::Spades -%}
+     {{ " black" }}
+   {%- when Suit::Diamonds or Suit::Hearts -%}
+     {{ " red" }}
+{%- endmatch %}
+
diff --git a/testing/tests/matches.rs b/testing/tests/matches.rs
index f5ccb95e7..45074891c 100644
--- a/testing/tests/matches.rs
+++ b/testing/tests/matches.rs
@@ -195,3 +195,32 @@ fn test_match_with_comment() {
     let s = MatchWithComment { good: false };
     assert_eq!(s.render().unwrap(), "bad");
 }
+
+enum Suit {
+    Clubs,
+    Diamonds,
+    Hearts,
+    Spades,
+}
+
+#[derive(Template)]
+#[template(path = "match-enum-or.html")]
+struct MatchEnumOrTemplate {
+    suit: Suit,
+}
+
+#[test]
+fn test_match_enum_or() {
+    let template = MatchEnumOrTemplate { suit: Suit::Clubs };
+    assert_eq!(template.render().unwrap(), "The card is black\n");
+    let template = MatchEnumOrTemplate { suit: Suit::Spades };
+    assert_eq!(template.render().unwrap(), "The card is black\n");
+
+    let template = MatchEnumOrTemplate { suit: Suit::Hearts };
+    assert_eq!(template.render().unwrap(), "The card is red\n");
+
+    let template = MatchEnumOrTemplate {
+        suit: Suit::Diamonds,
+    };
+    assert_eq!(template.render().unwrap(), "The card is red\n");
+}