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::*;