liquid_lib/stdlib/blocks/
case_block.rs

1use std::io::Write;
2
3use liquid_core::error::ResultLiquidExt;
4use liquid_core::model::{ValueView, ValueViewCmp};
5use liquid_core::parser::BlockElement;
6use liquid_core::parser::TryMatchToken;
7use liquid_core::Expression;
8use liquid_core::Language;
9use liquid_core::Renderable;
10use liquid_core::Result;
11use liquid_core::Runtime;
12use liquid_core::Template;
13use liquid_core::{BlockReflection, ParseBlock, TagBlock, TagTokenIter};
14
15#[derive(Copy, Clone, Debug, Default)]
16pub struct CaseBlock;
17
18impl CaseBlock {
19    pub fn new() -> Self {
20        Self
21    }
22}
23
24impl BlockReflection for CaseBlock {
25    fn start_tag(&self) -> &str {
26        "case"
27    }
28
29    fn end_tag(&self) -> &str {
30        "endcase"
31    }
32
33    fn description(&self) -> &str {
34        ""
35    }
36}
37
38impl ParseBlock for CaseBlock {
39    fn parse(
40        &self,
41        mut arguments: TagTokenIter<'_>,
42        mut tokens: TagBlock<'_, '_>,
43        options: &Language,
44    ) -> Result<Box<dyn Renderable>> {
45        let target = arguments
46            .expect_next("Value expected.")?
47            .expect_value()
48            .into_result()?;
49
50        // no more arguments should be supplied, trying to supply them is an error
51        arguments.expect_nothing()?;
52
53        let mut cases = Vec::new();
54        let mut else_block = None;
55        let mut current_block = Vec::new();
56        let mut current_condition = None;
57
58        while let Some(element) = tokens.next()? {
59            match element {
60                BlockElement::Tag(mut tag) => match tag.name() {
61                    "when" => {
62                        if let Some(condition) = current_condition {
63                            cases.push(CaseOption::new(condition, Template::new(current_block)));
64                        }
65                        current_block = Vec::new();
66                        current_condition = Some(parse_condition(tag.tokens())?);
67                    }
68                    "else" => {
69                        // no more arguments should be supplied, trying to supply them is an error
70                        tag.tokens().expect_nothing()?;
71                        else_block = Some(tokens.parse_all(options)?);
72                        break;
73                    }
74                    _ => current_block.push(tag.parse(&mut tokens, options)?),
75                },
76                element => current_block.push(element.parse(&mut tokens, options)?),
77            }
78        }
79
80        if let Some(condition) = current_condition {
81            cases.push(CaseOption::new(condition, Template::new(current_block)));
82        }
83
84        let else_block = else_block.map(Template::new);
85
86        tokens.assert_empty();
87        Ok(Box::new(Case {
88            target,
89            cases,
90            else_block,
91        }))
92    }
93
94    fn reflection(&self) -> &dyn BlockReflection {
95        self
96    }
97}
98
99fn parse_condition(arguments: &mut TagTokenIter<'_>) -> Result<Vec<Expression>> {
100    let mut values = Vec::new();
101
102    let first_value = arguments
103        .expect_next("Value expected")?
104        .expect_value()
105        .into_result()?;
106    values.push(first_value);
107
108    while let Some(token) = arguments.next() {
109        if let TryMatchToken::Fails(token) = token.expect_str("or") {
110            token
111                .expect_str(",")
112                .into_result_custom_msg("\"or\" or \",\" expected.")?;
113        }
114
115        let value = arguments
116            .expect_next("Value expected")?
117            .expect_value()
118            .into_result()?;
119        values.push(value);
120    }
121
122    // no more arguments should be supplied, trying to supply them is an error
123    arguments.expect_nothing()?;
124    Ok(values)
125}
126
127#[derive(Debug)]
128struct Case {
129    target: Expression,
130    cases: Vec<CaseOption>,
131    else_block: Option<Template>,
132}
133
134impl Case {
135    fn trace(&self) -> String {
136        format!("{{% case {} %}}", self.target)
137    }
138}
139
140impl Renderable for Case {
141    fn render_to(&self, writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()> {
142        let value = self.target.evaluate(runtime)?.to_value();
143        for case in &self.cases {
144            if case.evaluate(&value, runtime)? {
145                return case
146                    .template
147                    .render_to(writer, runtime)
148                    .trace_with(|| case.trace().into())
149                    .trace_with(|| self.trace().into())
150                    .context_key_with(|| self.target.to_string().into())
151                    .value_with(|| value.to_kstr().into_owned());
152            }
153        }
154
155        if let Some(ref t) = self.else_block {
156            return t
157                .render_to(writer, runtime)
158                .trace("{{% else %}}")
159                .trace_with(|| self.trace().into())
160                .context_key_with(|| self.target.to_string().into())
161                .value_with(|| value.to_kstr().into_owned());
162        }
163
164        Ok(())
165    }
166}
167
168#[derive(Debug)]
169struct CaseOption {
170    args: Vec<Expression>,
171    template: Template,
172}
173
174impl CaseOption {
175    fn new(args: Vec<Expression>, template: Template) -> CaseOption {
176        CaseOption { args, template }
177    }
178
179    fn evaluate(&self, value: &dyn ValueView, runtime: &dyn Runtime) -> Result<bool> {
180        for a in &self.args {
181            let v = a.evaluate(runtime)?;
182            if v == ValueViewCmp::new(value) {
183                return Ok(true);
184            }
185        }
186        Ok(false)
187    }
188
189    fn trace(&self) -> String {
190        format!("{{% when {} %}}", itertools::join(self.args.iter(), " or "))
191    }
192}
193
194#[cfg(test)]
195mod test {
196    use super::*;
197
198    use liquid_core::model::Value;
199    use liquid_core::parser;
200    use liquid_core::runtime::RuntimeBuilder;
201
202    fn options() -> Language {
203        let mut options = Language::default();
204        options.blocks.register("case".to_owned(), CaseBlock.into());
205        options
206    }
207
208    #[test]
209    fn test_case_block() {
210        let text = concat!(
211            "{% case x %}",
212            "{% when 2 %}",
213            "two",
214            "{% when 3 or 4 %}",
215            "three and a half",
216            "{% else %}",
217            "otherwise",
218            "{% endcase %}"
219        );
220        let options = options();
221        let template = parser::parse(text, &options).map(Template::new).unwrap();
222
223        let runtime = RuntimeBuilder::new().build();
224        runtime.set_global("x".into(), Value::scalar(2f64));
225        assert_eq!(template.render(&runtime).unwrap(), "two");
226
227        runtime.set_global("x".into(), Value::scalar(3f64));
228        assert_eq!(template.render(&runtime).unwrap(), "three and a half");
229
230        runtime.set_global("x".into(), Value::scalar(4f64));
231        assert_eq!(template.render(&runtime).unwrap(), "three and a half");
232
233        runtime.set_global("x".into(), Value::scalar("nope"));
234        assert_eq!(template.render(&runtime).unwrap(), "otherwise");
235    }
236
237    #[test]
238    fn test_no_matches_returns_empty_string() {
239        let text = concat!(
240            "{% case x %}",
241            "{% when 2 %}",
242            "two",
243            "{% when 3 or 4 %}",
244            "three and a half",
245            "{% endcase %}"
246        );
247        let options = options();
248        let template = parser::parse(text, &options).map(Template::new).unwrap();
249
250        let runtime = RuntimeBuilder::new().build();
251        runtime.set_global("x".into(), Value::scalar("nope"));
252        assert_eq!(template.render(&runtime).unwrap(), "");
253    }
254
255    #[test]
256    fn multiple_else_blocks_is_an_error() {
257        let text = concat!(
258            "{% case x %}",
259            "{% when 2 %}",
260            "two",
261            "{% else %}",
262            "else #1",
263            "{% else %}",
264            "else # 2",
265            "{% endcase %}"
266        );
267        let options = options();
268        let template = parser::parse(text, &options).map(Template::new);
269        assert!(template.is_err());
270    }
271}