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
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::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}