minijinja/
tests.rs

1//! Test functions and abstractions.
2//!
3//! Test functions in MiniJinja are like [`filters`](crate::filters) but a
4//! different syntax is used to invoke them and they have to return boolean
5//! values.  For instance the expression `{% if foo is odd %}` invokes the
6//! [`is_odd`] test to check if the value is indeed an odd number.
7//!
8//! MiniJinja comes with some built-in test functions that are listed below. To
9//! create a custom test write a function that takes at least a value argument
10//! that returns a boolean result, then register it with
11//! [`add_filter`](crate::Environment::add_test).
12//!
13//! # Using Tests
14//!
15//! Tests are useful to "test" a value in a specific way.  For instance if
16//! you want to assign different classes to alternating rows one way is
17//! using the `odd` test:
18//!
19//! ```jinja
20//! {% if seq is defined %}
21//!   <ul>
22//!   {% for item in seq %}
23//!     <li class="{{ 'even' if loop.index is even else 'odd' }}">{{ item }}</li>
24//!   {% endfor %}
25//!   </ul>
26//! {% endif %}
27//! ```
28//!
29//! # Custom Tests
30//!
31//! A custom test function is just a simple function which accepts its
32//! inputs as parameters and then returns a bool.  For instance the following
33//! shows a test function which takes an input value and checks if it's
34//! lowercase:
35//!
36//! ```
37//! # use minijinja::Environment;
38//! # let mut env = Environment::new();
39//! fn is_lowercase(value: String) -> bool {
40//!     value.chars().all(|x| x.is_lowercase())
41//! }
42//!
43//! env.add_test("lowercase", is_lowercase);
44//! ```
45//!
46//! MiniJinja will perform the necessary conversions automatically.  For more
47//! information see the [`Function`](crate::functions::Function) trait.  If a
48//! test returns a value that is not a bool, it will be evaluated for truthiness
49//! with [`Value::is_true`].
50//!
51//! # Built-in Tests
52//!
53//! When the `builtins` feature is enabled a range of built-in tests are
54//! automatically added to the environment.  These are also all provided in
55//! this module.  Note though that these functions are not to be
56//! called from Rust code as their exact interface (arguments and return types)
57//! might change from one MiniJinja version to another.
58use crate::error::Error;
59use crate::value::Value;
60use crate::vm::State;
61
62/// Deprecated alias
63#[deprecated = "Use the minijinja::functions::Function instead"]
64#[doc(hidden)]
65pub use crate::functions::Function as Test;
66#[deprecated = "Use the minijinja::value::FunctionResult instead"]
67#[doc(hidden)]
68pub use crate::value::FunctionResult as TestResult;
69
70/// Checks if a value is undefined.
71///
72/// ```jinja
73/// {{ 42 is undefined }} -> false
74/// ```
75pub fn is_undefined(v: &Value) -> bool {
76    v.is_undefined()
77}
78
79/// Checks if a value is defined.
80///
81/// ```jinja
82/// {{ 42 is defined }} -> true
83/// ```
84pub fn is_defined(v: &Value) -> bool {
85    !v.is_undefined()
86}
87
88/// Checks if a value is none.
89///
90/// ```jinja
91/// {{ none is none }} -> true
92/// ```
93pub fn is_none(v: &Value) -> bool {
94    v.is_none()
95}
96
97/// Checks if a value is safe.
98///
99/// ```jinja
100/// {{ "<hello>"|escape is safe }} -> true
101/// ```
102///
103/// This filter is also registered with the `escaped` alias.
104pub fn is_safe(v: &Value) -> bool {
105    v.is_safe()
106}
107
108#[cfg(feature = "builtins")]
109mod builtins {
110    use super::*;
111
112    use std::borrow::Cow;
113
114    use crate::value::ops::{coerce, CoerceResult};
115    use crate::value::ValueKind;
116
117    /// Return true if the object is a boolean value.
118    ///
119    /// ```jinja
120    /// {{ true is boolean }} -> true
121    /// ```
122    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
123    pub fn is_boolean(v: &Value) -> bool {
124        v.kind() == ValueKind::Bool
125    }
126
127    /// Checks if a value is odd.
128    ///
129    /// ```jinja
130    /// {{ 41 is odd }} -> true
131    /// ```
132    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
133    pub fn is_odd(v: Value) -> bool {
134        i128::try_from(v).ok().is_some_and(|x| x % 2 != 0)
135    }
136
137    /// Checks if a value is even.
138    ///
139    /// ```jinja
140    /// {{ 42 is even }} -> true
141    /// ```
142    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
143    pub fn is_even(v: Value) -> bool {
144        i128::try_from(v).ok().is_some_and(|x| x % 2 == 0)
145    }
146
147    /// Return true if the value is divisible by another one.
148    ///
149    /// ```jinja
150    /// {{ 42 is divisibleby(2) }} -> true
151    /// ```
152    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
153    pub fn is_divisibleby(v: &Value, other: &Value) -> bool {
154        match coerce(v, other, false) {
155            Some(CoerceResult::I128(a, b)) => (a % b) == 0,
156            Some(CoerceResult::F64(a, b)) => (a % b) == 0.0,
157            _ => false,
158        }
159    }
160
161    /// Checks if this value is a number.
162    ///
163    /// ```jinja
164    /// {{ 42 is number }} -> true
165    /// {{ "42" is number }} -> false
166    /// ```
167    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
168    pub fn is_number(v: &Value) -> bool {
169        matches!(v.kind(), ValueKind::Number)
170    }
171
172    /// Checks if this value is an integer.
173    ///
174    /// ```jinja
175    /// {{ 42 is integer }} -> true
176    /// {{ 42.0 is integer }} -> false
177    /// ```
178    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
179    pub fn is_integer(v: &Value) -> bool {
180        v.is_integer()
181    }
182
183    /// Checks if this value is a float
184    ///
185    /// ```jinja
186    /// {{ 42 is float }} -> false
187    /// {{ 42.0 is float }} -> true
188    /// ```
189    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
190    pub fn is_float(v: &Value) -> bool {
191        matches!(v.0, crate::value::ValueRepr::F64(_))
192    }
193
194    /// Checks if this value is a string.
195    ///
196    /// ```jinja
197    /// {{ "42" is string }} -> true
198    /// {{ 42 is string }} -> false
199    /// ```
200    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
201    pub fn is_string(v: &Value) -> bool {
202        matches!(v.kind(), ValueKind::String)
203    }
204
205    /// Checks if this value is a sequence
206    ///
207    /// ```jinja
208    /// {{ [1, 2, 3] is sequence }} -> true
209    /// {{ 42 is sequence }} -> false
210    /// ```
211    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
212    pub fn is_sequence(v: &Value) -> bool {
213        matches!(v.kind(), ValueKind::Seq)
214    }
215
216    /// Checks if this value can be iterated over.
217    ///
218    /// ```jinja
219    /// {{ [1, 2, 3] is iterable }} -> true
220    /// {{ 42 is iterable }} -> false
221    /// ```
222    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
223    pub fn is_iterable(v: &Value) -> bool {
224        v.try_iter().is_ok()
225    }
226
227    /// Checks if this value is a mapping
228    ///
229    /// ```jinja
230    /// {{ {"foo": "bar"} is mapping }} -> true
231    /// {{ [1, 2, 3] is mapping }} -> false
232    /// ```
233    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
234    pub fn is_mapping(v: &Value) -> bool {
235        matches!(v.kind(), ValueKind::Map)
236    }
237
238    /// Checks if the value is starting with a string.
239    ///
240    /// ```jinja
241    /// {{ "foobar" is startingwith "foo" }} -> true
242    /// {{ "foobar" is startingwith "bar" }} -> false
243    /// ```
244    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
245    pub fn is_startingwith(v: Cow<'_, str>, other: Cow<'_, str>) -> bool {
246        v.starts_with(&other as &str)
247    }
248
249    /// Checks if the value is ending with a string.
250    ///
251    /// ```jinja
252    /// {{ "foobar" is endingwith "bar" }} -> true
253    /// {{ "foobar" is endingwith "foo" }} -> false
254    /// ```
255    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
256    pub fn is_endingwith(v: Cow<'_, str>, other: Cow<'_, str>) -> bool {
257        v.ends_with(&other as &str)
258    }
259
260    /// Test version of `==`.
261    ///
262    /// This is useful when combined with [`select`](crate::filters::select).
263    ///
264    /// ```jinja
265    /// {{ 1 is eq 1 }} -> true
266    /// {{ [1, 2, 3]|select("==", 1) }} => [1]
267    /// ```
268    ///
269    /// By default aliased to `equalto` and `==`.
270    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
271    #[cfg(feature = "builtins")]
272    pub fn is_eq(value: &Value, other: &Value) -> bool {
273        *value == *other
274    }
275
276    /// Test version of `!=`.
277    ///
278    /// This is useful when combined with [`select`](crate::filters::select).
279    ///
280    /// ```jinja
281    /// {{ 2 is ne 1 }} -> true
282    /// {{ [1, 2, 3]|select("!=", 1) }} => [2, 3]
283    /// ```
284    ///
285    /// By default aliased to `!=`.
286    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
287    #[cfg(feature = "builtins")]
288    pub fn is_ne(value: &Value, other: &Value) -> bool {
289        *value != *other
290    }
291
292    /// Test version of `<`.
293    ///
294    /// This is useful when combined with [`select`](crate::filters::select).
295    ///
296    /// ```jinja
297    /// {{ 1 is lt 2 }} -> true
298    /// {{ [1, 2, 3]|select("<", 2) }} => [1]
299    /// ```
300    ///
301    /// By default aliased to `lessthan` and `<`.
302    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
303    #[cfg(feature = "builtins")]
304    pub fn is_lt(value: &Value, other: &Value) -> bool {
305        *value < *other
306    }
307
308    /// Test version of `<=`.
309    ///
310    /// This is useful when combined with [`select`](crate::filters::select).
311    ///
312    /// ```jinja
313    /// {{ 1 is le 2 }} -> true
314    /// {{ [1, 2, 3]|select("<=", 2) }} => [1, 2]
315    /// ```
316    ///
317    /// By default aliased to `<=`.
318    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
319    #[cfg(feature = "builtins")]
320    pub fn is_le(value: &Value, other: &Value) -> bool {
321        *value <= *other
322    }
323
324    /// Test version of `>`.
325    ///
326    /// This is useful when combined with [`select`](crate::filters::select).
327    ///
328    /// ```jinja
329    /// {{ 2 is gt 1 }} -> true
330    /// {{ [1, 2, 3]|select(">", 2) }} => [3]
331    /// ```
332    ///
333    /// By default aliased to `greaterthan` and `>`.
334    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
335    #[cfg(feature = "builtins")]
336    pub fn is_gt(value: &Value, other: &Value) -> bool {
337        *value > *other
338    }
339
340    /// Test version of `>=`.
341    ///
342    /// This is useful when combined with [`select`](crate::filters::select).
343    ///
344    /// ```jinja
345    /// {{ 2 is ge 1 }} -> true
346    /// {{ [1, 2, 3]|select(">=", 2) }} => [2, 3]
347    /// ```
348    ///
349    /// By default aliased to `>=`.
350    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
351    #[cfg(feature = "builtins")]
352    pub fn is_ge(value: &Value, other: &Value) -> bool {
353        *value >= *other
354    }
355
356    /// Test version of `in`.
357    ///
358    /// ```jinja
359    /// {{ 1 is in [1, 2, 3] }} -> true
360    /// {{ [1, 2, 3]|select("in", [1, 2]) }} => [1, 2]
361    /// ```
362    ///
363    /// This is useful when combined with [`select`](crate::filters::select).
364    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
365    #[cfg(feature = "builtins")]
366    pub fn is_in(state: &State, value: &Value, other: &Value) -> Result<bool, Error> {
367        ok!(state.undefined_behavior().assert_iterable(other));
368        Ok(crate::value::ops::contains(other, value)
369            .map(|value| value.is_true())
370            .unwrap_or(false))
371    }
372
373    /// Checks if a value is `true`.
374    ///
375    /// ```jinja
376    /// {% if value is true %}...{% endif %}
377    /// ```
378    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
379    #[cfg(feature = "builtins")]
380    pub fn is_true(value: &Value) -> bool {
381        matches!(value.0, crate::value::ValueRepr::Bool(true))
382    }
383
384    /// Checks if a value is `false`.
385    ///
386    /// ```jinja
387    /// {% if value is false %}...{% endif %}
388    /// ```
389    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
390    #[cfg(feature = "builtins")]
391    pub fn is_false(value: &Value) -> bool {
392        matches!(value.0, crate::value::ValueRepr::Bool(false))
393    }
394
395    /// Checks if a filter with a given name is available.
396    ///
397    /// ```jinja
398    /// {% if 'tojson' is filter %}...{% endif %}
399    /// ```
400    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
401    #[cfg(feature = "builtins")]
402    pub fn is_filter(state: &State, name: &str) -> bool {
403        state.env().get_filter(name).is_some()
404    }
405
406    /// Checks if a test with a given name is available.
407    ///
408    /// ```jinja
409    /// {% if 'greaterthan' is test %}...{% endif %}
410    /// ```
411    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
412    #[cfg(feature = "builtins")]
413    pub fn is_test(state: &State, name: &str) -> bool {
414        state.env().get_test(name).is_some()
415    }
416
417    /// Checks if a string is all lowercase.
418    ///
419    /// ```jinja
420    /// {{ 'foo' is lower }} -> true
421    /// ```
422    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
423    #[cfg(feature = "builtins")]
424    pub fn is_lower(name: &str) -> bool {
425        name.chars().all(|x| x.is_lowercase())
426    }
427
428    /// Checks if a string is all uppercase.
429    ///
430    /// ```jinja
431    /// {{ 'FOO' is upper }} -> true
432    /// ```
433    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
434    #[cfg(feature = "builtins")]
435    pub fn is_upper(name: &str) -> bool {
436        name.chars().all(|x| x.is_uppercase())
437    }
438
439    /// Checks if two values are identical.
440    ///
441    /// This primarily exists for compatibilith with Jinja2.  It can be seen as a much
442    /// stricter comparison than a regular comparison.  The main difference is that
443    /// values that have the same structure but a different internal object will not
444    /// compare equal.
445    ///
446    /// ```jinja
447    /// {{ [1, 2, 3] is sameas [1, 2, 3] }}
448    ///     -> false
449    ///
450    /// {{ false is sameas false }}
451    ///     -> true
452    /// ```
453    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
454    #[cfg(feature = "builtins")]
455    pub fn is_sameas(value: &Value, other: &Value) -> bool {
456        match (value.as_object(), other.as_object()) {
457            (Some(a), Some(b)) => a.is_same_object(b),
458            (None, Some(_)) | (Some(_), None) => false,
459            (None, None) => {
460                if value.kind() != other.kind() || value.is_integer() != other.is_integer() {
461                    false
462                } else {
463                    value == other
464                }
465            }
466        }
467    }
468}
469
470#[cfg(feature = "builtins")]
471pub use self::builtins::*;