Repository: mitsuhiko/minijinja
Pull request #529 (2024-06-23 21:03:13)

Added some missing tests to minijinja
This adds the missing tests `boolean`, `divisibleby`, `lower` and `upper`.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 91c3e16..99d99d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ All notable changes to MiniJinja are documented here.
 - Added new methods to pycompat: `str.endswith`, `str.rfind`,
   `str.isalnum`, `str.isalpha`, `str.isascii`, `str.isdigit`,
   `str.isnumeric`, `str.join`, `str.startswith`.  #522
+- Added the missing tests `boolean`, `divisibleby`, `lower` and `upper`.  #592
 
 ## 2.0.2
 
diff --git a/minijinja/src/defaults.rs b/minijinja/src/defaults.rs
index 2d8e797..ae5a54a 100644
--- a/minijinja/src/defaults.rs
+++ b/minijinja/src/defaults.rs
@@ -121,8 +121,10 @@ pub(crate) fn get_builtin_tests() -> BTreeMap<Cow<'static, str>, BoxedTest> {
     rv.insert("escaped".into(), is_safe);
     #[cfg(feature = "builtins")]
     {
+        rv.insert("boolean".into(), BoxedTest::new(tests::is_boolean));
         rv.insert("odd".into(), BoxedTest::new(tests::is_odd));
         rv.insert("even".into(), BoxedTest::new(tests::is_even));
+        rv.insert("divisibleby".into(), BoxedTest::new(tests::is_divisibleby));
         rv.insert("number".into(), BoxedTest::new(tests::is_number));
         rv.insert("integer".into(), BoxedTest::new(tests::is_integer));
         rv.insert("int".into(), BoxedTest::new(tests::is_integer));
@@ -136,6 +138,8 @@ pub(crate) fn get_builtin_tests() -> BTreeMap<Cow<'static, str>, BoxedTest> {
             BoxedTest::new(tests::is_startingwith),
         );
         rv.insert("endingwith".into(), BoxedTest::new(tests::is_endingwith));
+        rv.insert("lower".into(), BoxedTest::new(tests::is_lower));
+        rv.insert("upper".into(), BoxedTest::new(tests::is_upper));
 
         // operators
         let is_eq = BoxedTest::new(tests::is_eq);


Test patch:
diff --git a/minijinja/src/tests.rs b/minijinja/src/tests.rs
index 5ec1d88..16d677e 100644
--- a/minijinja/src/tests.rs
+++ b/minijinja/src/tests.rs
@@ -241,8 +241,19 @@ mod builtins {
 
     use std::borrow::Cow;
 
+    use crate::value::ops::{coerce, CoerceResult};
     use crate::value::ValueKind;
 
+    /// Return true if the object is a boolean value.
+    ///
+    /// ```jinja
+    /// {{ true is boolean }} -> true
+    /// ```
+    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
+    pub fn is_boolean(v: Value) -> bool {
+        v.kind() == ValueKind::Bool
+    }
+
     /// Checks if a value is odd.
     ///
     /// ```jinja
@@ -263,6 +274,20 @@ mod builtins {
         i128::try_from(v).ok().map_or(false, |x| x % 2 == 0)
     }
 
+    /// Return true if the value is divisible by another one.
+    ///
+    /// ```jinja
+    /// {{ 42 is divisibleby(2) }} -> true
+    /// ```
+    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
+    pub fn is_divisibleby(v: Value, other: Value) -> bool {
+        match coerce(&v, &other) {
+            Some(CoerceResult::I128(a, b)) => (a % b) == 0,
+            Some(CoerceResult::F64(a, b)) => (a % b) == 0.0,
+            _ => false,
+        }
+    }
+
     /// Checks if this value is a number.
     ///
     /// ```jinja
@@ -524,6 +549,28 @@ mod builtins {
     pub fn is_test(state: &State, name: &str) -> bool {
         state.env.get_test(name).is_some()
     }
+
+    /// Checks if a string is all lowercase.
+    ///
+    /// ```jinja
+    /// {{ 'foo' is lower }} -> true
+    /// ```
+    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
+    #[cfg(feature = "builtins")]
+    pub fn is_lower(name: &str) -> bool {
+        name.chars().all(|x| x.is_lowercase())
+    }
+
+    /// Checks if a string is all uppercase.
+    ///
+    /// ```jinja
+    /// {{ 'FOO' is upper }} -> true
+    /// ```
+    #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
+    #[cfg(feature = "builtins")]
+    pub fn is_upper(name: &str) -> bool {
+        name.chars().all(|x| x.is_uppercase())
+    }
 }
 
 #[cfg(feature = "builtins")]
diff --git a/minijinja/tests/inputs/tests.txt b/minijinja/tests/inputs/tests.txt
index 5b58851..788eeb2 100644
--- a/minijinja/tests/inputs/tests.txt
+++ b/minijinja/tests/inputs/tests.txt
@@ -45,3 +45,7 @@ is-true: {{ true is true }} | {{ 42 is true }}
 is-false: {{ false is false }} | {{ 0 is false }}
 is-filter: {{ 'escape' is filter }} | {{ 'unknown-filter' is filter }}
 is-test: {{ 'safe' is test }} | {{ 'unknown-test' is test }}
+is-boolean: {{ true is boolean }} | {{ 42 is boolean }}
+is-divisibleby: {{ 42 is divisibleby(2) }} | {{ 41 is divisibleby(2) }}
+is-lower: {{ "foo" is lower }} | {{ "FOO" is lower }}
+is-upper: {{ "foo" is upper }} | {{ "FOO" is upper }}
\ No newline at end of file
diff --git a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
index 879df19..62fa94e 100644
--- a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
@@ -36,7 +36,9 @@ State {
             "==",
             ">",
             ">=",
+            "boolean",
             "defined",
+            "divisibleby",
             "endingwith",
             "eq",
             "equalto",
@@ -54,6 +56,7 @@ State {
             "iterable",
             "le",
             "lessthan",
+            "lower",
             "lt",
             "mapping",
             "ne",
@@ -67,6 +70,7 @@ State {
             "test",
             "true",
             "undefined",
+            "upper",
         ],
         filters: [
             "abs",
diff --git a/minijinja/tests/snapshots/test_templates__vm@tests.txt.snap b/minijinja/tests/snapshots/test_templates__vm@tests.txt.snap
index 03a613c..2fe0111 100644
--- a/minijinja/tests/snapshots/test_templates__vm@tests.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@tests.txt.snap
@@ -1,16 +1,16 @@
 ---
 source: minijinja/tests/test_templates.rs
-description: "even: {{ two is even }}\nodd: {{ two is odd }}\nundefined: {{ two is undefined }}\ndefined: {{ two is defined }}\nundefined2: {{ ohwell is undefined }}\ndefined2: {{ ohwell is defined }}\nnone: {{ none is none }}\nnot-none: {{ 42 is not none }}\nnumber-int: {{ two is number }}\nnumber-float: {{ two_dot_two is number }}\ninteger-int: {{ 42 is integer }}\ninteger-float: {{ 42.0 is integer }}\nfloat-int: {{ 42 is float }}\nfloat-float: {{ 42.0 is float }}\nnot-seq: {{ two is sequence }}\nseq: {{ seq is sequence }}\nreverse-not-seq: {{ seq|reverse is sequence }}\niterable: {{ seq is iterable }}\niterable-reverse: {{ seq|reverse is iterable }}\nstring-iterable: {{ string is iterable }}\nnot-iterable: {{ two is iterable }}\nnot-map: {{ two is mapping }}\nmap: {{ map is mapping }}\nstring: {{ string is string }}\nnot-string: {{ mapping is string }}\nstarts-with-a: {{ string is startingwith('a') }}\nends-with-ha: {{ string is endingwith('ha') }}\nnot-safe: {{ \"foo\" is safe }}\nsafe: {{ \"foo\"|escape is safe }}\nis-true: {{ true is true }} | {{ 42 is true }}\nis-false: {{ false is false }} | {{ 0 is false }}\nis-filter: {{ 'escape' is filter }} | {{ 'unknown-filter' is filter }}\nis-test: {{ 'safe' is test }} | {{ 'unknown-test' is test }}"
+description: "even: {{ two is even }}\nodd: {{ two is odd }}\nundefined: {{ two is undefined }}\ndefined: {{ two is defined }}\nundefined2: {{ ohwell is undefined }}\ndefined2: {{ ohwell is defined }}\nnone: {{ none is none }}\nnot-none: {{ 42 is not none }}\nnumber-int: {{ two is number }}\nnumber-float: {{ two_dot_two is number }}\ninteger-int: {{ 42 is integer }}\ninteger-float: {{ 42.0 is integer }}\nfloat-int: {{ 42 is float }}\nfloat-float: {{ 42.0 is float }}\nnot-seq: {{ two is sequence }}\nseq: {{ seq is sequence }}\nreverse-not-seq: {{ seq|reverse is sequence }}\niterable: {{ seq is iterable }}\niterable-reverse: {{ seq|reverse is iterable }}\nstring-iterable: {{ string is iterable }}\nnot-iterable: {{ two is iterable }}\nnot-map: {{ two is mapping }}\nmap: {{ map is mapping }}\nstring: {{ string is string }}\nnot-string: {{ mapping is string }}\nstarts-with-a: {{ string is startingwith('a') }}\nends-with-ha: {{ string is endingwith('ha') }}\nnot-safe: {{ \"foo\" is safe }}\nsafe: {{ \"foo\"|escape is safe }}\nis-true: {{ true is true }} | {{ 42 is true }}\nis-false: {{ false is false }} | {{ 0 is false }}\nis-filter: {{ 'escape' is filter }} | {{ 'unknown-filter' is filter }}\nis-test: {{ 'safe' is test }} | {{ 'unknown-test' is test }}\nis-boolean: {{ true is boolean }} | {{ 42 is boolean }}\nis-divisibleby: {{ 42 is divisibleby(2) }} | {{ 41 is divisibleby(2) }}\nis-lower: {{ \"foo\" is lower }} | {{ \"FOO\" is lower }}\nis-upper: {{ \"foo\" is upper }} | {{ \"FOO\" is upper }}"
 info:
-  map:
-    foo: bar
+  two: 2
+  two_dot_two: 2.2
   seq:
     - 1
     - 2
     - 3
+  map:
+    foo: bar
   string: aha
-  two: 2
-  two_dot_two: 2.2
 input_file: minijinja/tests/inputs/tests.txt
 ---
 even: true
@@ -46,3 +46,7 @@ is-true: true | false
 is-false: true | false
 is-filter: true | false
 is-test: true | false
+is-boolean: true | false
+is-divisibleby: true | false
+is-lower: true | false
+is-upper: false | true