loose_liquid_lib/stdlib/blocks/
case_block.rs1use 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 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 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 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}