Skip to content

Commit a0d2720

Browse files
committed
feat: Implement contains operator
The contains operator works for strings (substring search), arrays (string in array) or objects (string key exists). The renderer will error if the right-hand side operator does not evaluate to a string, if the left-hand side is neither a string, array nor object or if the array contains non-string items. Fixes #155
1 parent 6c1c69f commit a0d2720

File tree

1 file changed

+163
-1
lines changed

1 file changed

+163
-1
lines changed

src/tags/if_block.rs

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use error::{Error, Result};
22

3+
use value::Value;
34
use interpreter::Argument;
45
use interpreter::Context;
56
use interpreter::Renderable;
@@ -25,6 +26,29 @@ struct Conditional {
2526
if_false: Option<Template>,
2627
}
2728

29+
fn contains_check(a: &Value, b: &Value) -> Result<bool> {
30+
let b = b.as_str()
31+
.ok_or("Right-hand side of contains operator must be a string")?;
32+
33+
match *a {
34+
Value::Str(ref val) => Ok(val.contains(b)),
35+
Value::Object(ref obj) => Ok(obj.contains_key(b)),
36+
Value::Array(ref arr) => {
37+
for elem in arr {
38+
let elem = elem.as_str()
39+
.ok_or("The contains operator can only check for strings")?;
40+
if elem == b {
41+
return Ok(true);
42+
}
43+
}
44+
Ok(false)
45+
}
46+
_ => {
47+
Error::renderer("Left-hand side of contains operator must be a string, array or object")
48+
}
49+
}
50+
}
51+
2852
impl Conditional {
2953
fn compare(&self, context: &Context) -> Result<bool> {
3054
let a = self.condition.lh.evaluate(context)?;
@@ -37,7 +61,7 @@ impl Conditional {
3761
ComparisonOperator::GreaterThan => a > b,
3862
ComparisonOperator::LessThanEquals => a <= b,
3963
ComparisonOperator::GreaterThanEquals => a >= b,
40-
ComparisonOperator::Contains => false, // TODO!!!
64+
ComparisonOperator::Contains => contains_check(&a, &b)?,
4165
};
4266

4367
Ok(result == self.mode)
@@ -133,6 +157,7 @@ pub fn if_block(_tag_name: &str,
133157

134158
#[cfg(test)]
135159
mod test {
160+
use std::collections::HashMap;
136161
use super::*;
137162
use value::Value;
138163
use compiler;
@@ -336,4 +361,141 @@ mod test {
336361
let output = template.render(&mut context).unwrap();
337362
assert_eq!(output, Some("fourth".to_owned()));
338363
}
364+
365+
#[test]
366+
fn string_contains_with_literals() {
367+
let text = "{% if \"Star Wars\" contains \"Star\" %}if true{% endif %}";
368+
let tokens = compiler::tokenize(&text).unwrap();
369+
let template = compiler::parse(&tokens, &options())
370+
.map(interpreter::Template::new)
371+
.unwrap();
372+
373+
let mut context = Context::new();
374+
let output = template.render(&mut context).unwrap();
375+
assert_eq!(output, Some("if true".to_owned()));
376+
377+
let text = "{% if \"Star Wars\" contains \"Alf\" %}if true{% else %}if false{% endif %}";
378+
let tokens = compiler::tokenize(&text).unwrap();
379+
let template = compiler::parse(&tokens, &options())
380+
.map(interpreter::Template::new)
381+
.unwrap();
382+
383+
let mut context = Context::new();
384+
let output = template.render(&mut context).unwrap();
385+
assert_eq!(output, Some("if false".to_owned()));
386+
}
387+
388+
#[test]
389+
fn string_contains_with_variables() {
390+
let text = "{% if movie contains \"Star\" %}if true{% endif %}";
391+
let tokens = compiler::tokenize(&text).unwrap();
392+
let template = compiler::parse(&tokens, &options())
393+
.map(interpreter::Template::new)
394+
.unwrap();
395+
396+
let mut context = Context::new();
397+
context.set_global_val("movie", Value::Str("Star Wars".into()));
398+
let output = template.render(&mut context).unwrap();
399+
assert_eq!(output, Some("if true".to_owned()));
400+
401+
let text = "{% if movie contains \"Star\" %}if true{% else %}if false{% endif %}";
402+
let tokens = compiler::tokenize(&text).unwrap();
403+
let template = compiler::parse(&tokens, &options())
404+
.map(interpreter::Template::new)
405+
.unwrap();
406+
407+
let mut context = Context::new();
408+
context.set_global_val("movie", Value::Str("Batman".into()));
409+
let output = template.render(&mut context).unwrap();
410+
assert_eq!(output, Some("if false".to_owned()));
411+
}
412+
413+
#[test]
414+
fn contains_numeric_lhs() {
415+
let text = "{% if 7 contains \"Star\" %}if true{% endif %}";
416+
let tokens = compiler::tokenize(&text).unwrap();
417+
let template = compiler::parse(&tokens, &options())
418+
.map(interpreter::Template::new)
419+
.unwrap();
420+
421+
let mut context = Context::new();
422+
let output = template.render(&mut context);
423+
assert!(output.is_err());
424+
}
425+
426+
#[test]
427+
fn contains_non_string_rhs() {
428+
let text = "{% if \"Star Wars\" contains 7 %}if true{% endif %}";
429+
let tokens = compiler::tokenize(&text).unwrap();
430+
let template = compiler::parse(&tokens, &options())
431+
.map(interpreter::Template::new)
432+
.unwrap();
433+
434+
let mut context = Context::new();
435+
let output = template.render(&mut context);
436+
assert!(output.is_err());
437+
}
438+
439+
#[test]
440+
fn contains_with_object_and_key() {
441+
let text = "{% if movies contains \"Star Wars\" %}if true{% endif %}";
442+
let tokens = compiler::tokenize(&text).unwrap();
443+
let template = compiler::parse(&tokens, &options())
444+
.map(interpreter::Template::new)
445+
.unwrap();
446+
447+
let mut context = Context::new();
448+
let mut obj = HashMap::new();
449+
obj.insert("Star Wars".to_owned(), Value::str("1977"));
450+
context.set_global_val("movies", Value::Object(obj));
451+
let output = template.render(&mut context).unwrap();
452+
assert_eq!(output, Some("if true".to_owned()));
453+
}
454+
455+
#[test]
456+
fn contains_with_object_and_missing_key() {
457+
let text = "{% if movies contains \"Star Wars\" %}if true{% else %}if false{% endif %}";
458+
let tokens = compiler::tokenize(&text).unwrap();
459+
let template = compiler::parse(&tokens, &options())
460+
.map(interpreter::Template::new)
461+
.unwrap();
462+
463+
let mut context = Context::new();
464+
let obj = HashMap::new();
465+
context.set_global_val("movies", Value::Object(obj));
466+
let output = template.render(&mut context).unwrap();
467+
assert_eq!(output, Some("if false".to_owned()));
468+
}
469+
470+
#[test]
471+
fn contains_with_array_and_match() {
472+
let text = "{% if movies contains \"Star Wars\" %}if true{% endif %}";
473+
let tokens = compiler::tokenize(&text).unwrap();
474+
let template = compiler::parse(&tokens, &options())
475+
.map(interpreter::Template::new)
476+
.unwrap();
477+
478+
let mut context = Context::new();
479+
let arr = vec![Value::str("Star Wars"),
480+
Value::str("Star Trek"),
481+
Value::str("Alien")];
482+
context.set_global_val("movies", Value::Array(arr));
483+
let output = template.render(&mut context).unwrap();
484+
assert_eq!(output, Some("if true".to_owned()));
485+
}
486+
487+
#[test]
488+
fn contains_with_array_and_no_match() {
489+
let text = "{% if movies contains \"Star Wars\" %}if true{% else %}if false{% endif %}";
490+
let tokens = compiler::tokenize(&text).unwrap();
491+
let template = compiler::parse(&tokens, &options())
492+
.map(interpreter::Template::new)
493+
.unwrap();
494+
495+
let mut context = Context::new();
496+
let arr = vec![Value::str("Alien")];
497+
context.set_global_val("movies", Value::Array(arr));
498+
let output = template.render(&mut context).unwrap();
499+
assert_eq!(output, Some("if false".to_owned()));
500+
}
339501
}

0 commit comments

Comments
 (0)