loose_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::default()
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;
201    use liquid_core::runtime::RuntimeBuilder;
202
203    fn options() -> Language {
204        let mut options = Language::default();
205        options
206            .blocks
207            .register("case".to_string(), CaseBlock.into());
208        options
209    }
210
211    #[test]
212    fn test_case_block() {
213        let text = concat!(
214            "{% case x %}",
215            "{% when 2 %}",
216            "two",
217            "{% when 3 or 4 %}",
218            "three and a half",
219            "{% else %}",
220            "otherwise",
221            "{% endcase %}"
222        );
223        let options = options();
224        let template = parser::parse(text, &options)
225            .map(runtime::Template::new)
226            .unwrap();
227
228        let runtime = RuntimeBuilder::new().build();
229        runtime.set_global("x".into(), Value::scalar(2f64));
230        assert_eq!(template.render(&runtime).unwrap(), "two");
231
232        runtime.set_global("x".into(), Value::scalar(3f64));
233        assert_eq!(template.render(&runtime).unwrap(), "three and a half");
234
235        runtime.set_global("x".into(), Value::scalar(4f64));
236        assert_eq!(template.render(&runtime).unwrap(), "three and a half");
237
238        runtime.set_global("x".into(), Value::scalar("nope"));
239        assert_eq!(template.render(&runtime).unwrap(), "otherwise");
240    }
241
242    #[test]
243    fn test_no_matches_returns_empty_string() {
244        let text = concat!(
245            "{% case x %}",
246            "{% when 2 %}",
247            "two",
248            "{% when 3 or 4 %}",
249            "three and a half",
250            "{% endcase %}"
251        );
252        let options = options();
253        let template = parser::parse(text, &options)
254            .map(runtime::Template::new)
255            .unwrap();
256
257        let runtime = RuntimeBuilder::new().build();
258        runtime.set_global("x".into(), Value::scalar("nope"));
259        assert_eq!(template.render(&runtime).unwrap(), "");
260    }
261
262    #[test]
263    fn multiple_else_blocks_is_an_error() {
264        let text = concat!(
265            "{% case x %}",
266            "{% when 2 %}",
267            "two",
268            "{% else %}",
269            "else #1",
270            "{% else %}",
271            "else # 2",
272            "{% endcase %}"
273        );
274        let options = options();
275        let template = parser::parse(text, &options).map(runtime::Template::new);
276        assert!(template.is_err());
277    }
278}