tera/builtins/
testers.rs

1use crate::context::ValueNumber;
2use crate::errors::{Error, Result};
3use regex::Regex;
4use serde_json::value::Value;
5
6/// The tester function type definition
7pub trait Test: Sync + Send {
8    /// The tester function type definition
9    fn test(&self, value: Option<&Value>, args: &[Value]) -> Result<bool>;
10}
11
12impl<F> Test for F
13where
14    F: Fn(Option<&Value>, &[Value]) -> Result<bool> + Sync + Send,
15{
16    fn test(&self, value: Option<&Value>, args: &[Value]) -> Result<bool> {
17        self(value, args)
18    }
19}
20
21/// Check that the number of args match what was expected
22pub fn number_args_allowed(tester_name: &str, max: usize, args_len: usize) -> Result<()> {
23    if max == 0 && args_len > max {
24        return Err(Error::msg(format!(
25            "Tester `{}` was called with some args but this test doesn't take args",
26            tester_name
27        )));
28    }
29
30    if args_len > max {
31        return Err(Error::msg(format!(
32            "Tester `{}` was called with {} args, the max number is {}",
33            tester_name, args_len, max
34        )));
35    }
36
37    Ok(())
38}
39
40/// Called to check if the Value is defined and return an Err if not
41pub fn value_defined(tester_name: &str, value: Option<&Value>) -> Result<()> {
42    if value.is_none() {
43        return Err(Error::msg(format!(
44            "Tester `{}` was called on an undefined variable",
45            tester_name
46        )));
47    }
48
49    Ok(())
50}
51
52/// Helper function to extract string from an [`Option<Value>`] to remove boilerplate
53/// with tester error handling
54pub fn extract_string<'a>(
55    tester_name: &str,
56    part: &str,
57    value: Option<&'a Value>,
58) -> Result<&'a str> {
59    match value.and_then(Value::as_str) {
60        Some(s) => Ok(s),
61        None => Err(Error::msg(format!(
62            "Tester `{}` was called {} that isn't a string",
63            tester_name, part
64        ))),
65    }
66}
67
68/// Returns true if `value` is defined. Otherwise, returns false.
69pub fn defined(value: Option<&Value>, params: &[Value]) -> Result<bool> {
70    number_args_allowed("defined", 0, params.len())?;
71
72    Ok(value.is_some())
73}
74
75/// Returns true if `value` is undefined. Otherwise, returns false.
76pub fn undefined(value: Option<&Value>, params: &[Value]) -> Result<bool> {
77    number_args_allowed("undefined", 0, params.len())?;
78
79    Ok(value.is_none())
80}
81
82/// Returns true if `value` is a string. Otherwise, returns false.
83pub fn string(value: Option<&Value>, params: &[Value]) -> Result<bool> {
84    number_args_allowed("string", 0, params.len())?;
85    value_defined("string", value)?;
86
87    match value {
88        Some(Value::String(_)) => Ok(true),
89        _ => Ok(false),
90    }
91}
92
93/// Returns true if `value` is a number. Otherwise, returns false.
94pub fn number(value: Option<&Value>, params: &[Value]) -> Result<bool> {
95    number_args_allowed("number", 0, params.len())?;
96    value_defined("number", value)?;
97
98    match value {
99        Some(Value::Number(_)) => Ok(true),
100        _ => Ok(false),
101    }
102}
103
104/// Returns true if `value` is an odd number. Otherwise, returns false.
105pub fn odd(value: Option<&Value>, params: &[Value]) -> Result<bool> {
106    number_args_allowed("odd", 0, params.len())?;
107    value_defined("odd", value)?;
108
109    match value.and_then(|v| v.to_number().ok()) {
110        Some(f) => Ok(f % 2.0 != 0.0),
111        _ => Err(Error::msg("Tester `odd` was called on a variable that isn't a number")),
112    }
113}
114
115/// Returns true if `value` is an even number. Otherwise, returns false.
116pub fn even(value: Option<&Value>, params: &[Value]) -> Result<bool> {
117    number_args_allowed("even", 0, params.len())?;
118    value_defined("even", value)?;
119
120    let is_odd = odd(value, params)?;
121    Ok(!is_odd)
122}
123
124/// Returns true if `value` is divisible by the first param. Otherwise, returns false.
125pub fn divisible_by(value: Option<&Value>, params: &[Value]) -> Result<bool> {
126    number_args_allowed("divisibleby", 1, params.len())?;
127    value_defined("divisibleby", value)?;
128
129    match value.and_then(|v| v.to_number().ok()) {
130        Some(val) => match params.first().and_then(|v| v.to_number().ok()) {
131            Some(p) => Ok(val % p == 0.0),
132            None => Err(Error::msg(
133                "Tester `divisibleby` was called with a parameter that isn't a number",
134            )),
135        },
136        None => {
137            Err(Error::msg("Tester `divisibleby` was called on a variable that isn't a number"))
138        }
139    }
140}
141
142/// Returns true if `value` can be iterated over in Tera (ie is an array/tuple or an object).
143/// Otherwise, returns false.
144pub fn iterable(value: Option<&Value>, params: &[Value]) -> Result<bool> {
145    number_args_allowed("iterable", 0, params.len())?;
146    value_defined("iterable", value)?;
147
148    Ok(value.unwrap().is_array() || value.unwrap().is_object())
149}
150
151/// Returns true if the given variable is an object (ie can be iterated over key, value).
152/// Otherwise, returns false.
153pub fn object(value: Option<&Value>, params: &[Value]) -> Result<bool> {
154    number_args_allowed("object", 0, params.len())?;
155    value_defined("object", value)?;
156
157    Ok(value.unwrap().is_object())
158}
159
160/// Returns true if `value` starts with the given string. Otherwise, returns false.
161pub fn starting_with(value: Option<&Value>, params: &[Value]) -> Result<bool> {
162    number_args_allowed("starting_with", 1, params.len())?;
163    value_defined("starting_with", value)?;
164
165    let value = extract_string("starting_with", "on a variable", value)?;
166    let needle = extract_string("starting_with", "with a parameter", params.first())?;
167    Ok(value.starts_with(needle))
168}
169
170/// Returns true if `value` ends with the given string. Otherwise, returns false.
171pub fn ending_with(value: Option<&Value>, params: &[Value]) -> Result<bool> {
172    number_args_allowed("ending_with", 1, params.len())?;
173    value_defined("ending_with", value)?;
174
175    let value = extract_string("ending_with", "on a variable", value)?;
176    let needle = extract_string("ending_with", "with a parameter", params.first())?;
177    Ok(value.ends_with(needle))
178}
179
180/// Returns true if `value` contains the given argument. Otherwise, returns false.
181pub fn containing(value: Option<&Value>, params: &[Value]) -> Result<bool> {
182    number_args_allowed("containing", 1, params.len())?;
183    value_defined("containing", value)?;
184
185    match value.unwrap() {
186        Value::String(v) => {
187            let needle = extract_string("containing", "with a parameter", params.first())?;
188            Ok(v.contains(needle))
189        }
190        Value::Array(v) => Ok(v.contains(params.first().unwrap())),
191        Value::Object(v) => {
192            let needle = extract_string("containing", "with a parameter", params.first())?;
193            Ok(v.contains_key(needle))
194        }
195        _ => Err(Error::msg("Tester `containing` can only be used on string, array or map")),
196    }
197}
198
199/// Returns true if `value` is a string and matches the regex in the argument. Otherwise, returns false.
200pub fn matching(value: Option<&Value>, params: &[Value]) -> Result<bool> {
201    number_args_allowed("matching", 1, params.len())?;
202    value_defined("matching", value)?;
203
204    let value = extract_string("matching", "on a variable", value)?;
205    let regex = extract_string("matching", "with a parameter", params.first())?;
206
207    let regex = match Regex::new(regex) {
208        Ok(regex) => regex,
209        Err(err) => {
210            return Err(Error::msg(format!(
211                "Tester `matching`: Invalid regular expression: {}",
212                err
213            )));
214        }
215    };
216
217    Ok(regex.is_match(value))
218}
219
220#[cfg(test)]
221mod tests {
222    use std::collections::HashMap;
223
224    use super::{
225        containing, defined, divisible_by, ending_with, iterable, matching, object, starting_with,
226        string,
227    };
228
229    use serde_json::value::to_value;
230
231    #[test]
232    fn test_number_args_ok() {
233        assert!(defined(None, &[]).is_ok())
234    }
235
236    #[test]
237    fn test_too_many_args() {
238        assert!(defined(None, &[to_value(1).unwrap()]).is_err())
239    }
240
241    #[test]
242    fn test_value_defined() {
243        assert!(string(None, &[]).is_err())
244    }
245
246    #[test]
247    fn test_divisible_by() {
248        let tests = vec![
249            (1.0, 2.0, false),
250            (4.0, 2.0, true),
251            (4.0, 2.1, false),
252            (10.0, 2.0, true),
253            (10.0, 0.0, false),
254        ];
255
256        for (val, divisor, expected) in tests {
257            assert_eq!(
258                divisible_by(Some(&to_value(val).unwrap()), &[to_value(divisor).unwrap()],)
259                    .unwrap(),
260                expected
261            );
262        }
263    }
264
265    #[test]
266    fn test_iterable() {
267        assert!(iterable(Some(&to_value(vec!["1"]).unwrap()), &[]).unwrap());
268        assert!(!iterable(Some(&to_value(1).unwrap()), &[]).unwrap());
269        assert!(!iterable(Some(&to_value("hello").unwrap()), &[]).unwrap());
270    }
271
272    #[test]
273    fn test_object() {
274        let mut h = HashMap::new();
275        h.insert("a", 1);
276        assert!(object(Some(&to_value(h).unwrap()), &[]).unwrap());
277        assert!(!object(Some(&to_value(1).unwrap()), &[]).unwrap());
278        assert!(!object(Some(&to_value("hello").unwrap()), &[]).unwrap());
279    }
280
281    #[test]
282    fn test_starting_with() {
283        assert!(starting_with(
284            Some(&to_value("helloworld").unwrap()),
285            &[to_value("hello").unwrap()],
286        )
287        .unwrap());
288        assert!(
289            !starting_with(Some(&to_value("hello").unwrap()), &[to_value("hi").unwrap()],).unwrap()
290        );
291    }
292
293    #[test]
294    fn test_ending_with() {
295        assert!(
296            ending_with(Some(&to_value("helloworld").unwrap()), &[to_value("world").unwrap()],)
297                .unwrap()
298        );
299        assert!(
300            !ending_with(Some(&to_value("hello").unwrap()), &[to_value("hi").unwrap()],).unwrap()
301        );
302    }
303
304    #[test]
305    fn test_containing() {
306        let mut map = HashMap::new();
307        map.insert("hey", 1);
308
309        let tests = vec![
310            (to_value("hello world").unwrap(), to_value("hel").unwrap(), true),
311            (to_value("hello world").unwrap(), to_value("hol").unwrap(), false),
312            (to_value(vec![1, 2, 3]).unwrap(), to_value(3).unwrap(), true),
313            (to_value(vec![1, 2, 3]).unwrap(), to_value(4).unwrap(), false),
314            (to_value(map.clone()).unwrap(), to_value("hey").unwrap(), true),
315            (to_value(map.clone()).unwrap(), to_value("ho").unwrap(), false),
316        ];
317
318        for (container, needle, expected) in tests {
319            assert_eq!(containing(Some(&container), &[needle]).unwrap(), expected);
320        }
321    }
322
323    #[test]
324    fn test_matching() {
325        let tests = vec![
326            (to_value("abc").unwrap(), to_value("b").unwrap(), true),
327            (to_value("abc").unwrap(), to_value("^b$").unwrap(), false),
328            (
329                to_value("Hello, World!").unwrap(),
330                to_value(r"(?i)(hello\W\sworld\W)").unwrap(),
331                true,
332            ),
333            (
334                to_value("The date was 2018-06-28").unwrap(),
335                to_value(r"\d{4}-\d{2}-\d{2}$").unwrap(),
336                true,
337            ),
338        ];
339
340        for (container, needle, expected) in tests {
341            assert_eq!(matching(Some(&container), &[needle]).unwrap(), expected);
342        }
343
344        assert!(
345            matching(Some(&to_value("").unwrap()), &[to_value("(Invalid regex").unwrap()]).is_err()
346        );
347    }
348}