Repository: mitsuhiko/minijinja
Pull request #107 (2022-09-04 20:39:36)

Simplified function interface
This simplifies the function interface and the documentation of it. You no longer need to return results from these functions but values can be returned directly if needed.


Patch:
diff --git a/minijinja/src/environment.rs b/minijinja/src/environment.rs
index 5ca3570..9bb2377 100644
--- a/minijinja/src/environment.rs
+++ b/minijinja/src/environment.rs
@@ -9,7 +9,7 @@ use crate::error::Error;
 use crate::instructions::Instructions;
 use crate::parser::{parse, parse_expr};
 use crate::utils::{AutoEscape, BTreeMapKeysDebug, HtmlEscape};
-use crate::value::{ArgType, FunctionArgs, Value};
+use crate::value::{ArgType, FunctionArgs, FunctionResult, Value};
 use crate::vm::Vm;
 use crate::{filters, functions, tests};
 
@@ -534,12 +534,14 @@ impl<'source> Environment<'source> {
 
     /// Adds a new filter function.
     ///
-    /// For details about filters have a look at [`filters`].
+    /// Filter functions are functions that can be applied to values in
+    /// templates.  For details about filters have a look at
+    /// [`Filter`](crate::filters::Filter).
     pub fn add_filter<F, V, Rv, Args>(&mut self, name: &'source str, f: F)
     where
-        V: for<'a> ArgType<'a>,
-        Rv: Into<Value>,
         F: filters::Filter<V, Rv, Args>,
+        V: for<'a> ArgType<'a>,
+        Rv: FunctionResult,
         Args: for<'a> FunctionArgs<'a>,
     {
         self.filters.insert(name, filters::BoxedFilter::new(f));
@@ -552,11 +554,14 @@ impl<'source> Environment<'source> {
 
     /// Adds a new test function.
     ///
-    /// For details about tests have a look at [`tests`].
-    pub fn add_test<F, V, Args>(&mut self, name: &'source str, f: F)
+    /// Test functions are similar to filters but perform a check on a value
+    /// where the return value is always true or false.  For details about tests
+    /// have a look at [`Test`](crate::tests::Test).
+    pub fn add_test<F, V, Rv, Args>(&mut self, name: &'source str, f: F)
     where
         V: for<'a> ArgType<'a>,
-        F: tests::Test<V, Args>,
+        Rv: tests::TestResult,
+        F: tests::Test<V, Rv, Args>,
         Args: for<'a> FunctionArgs<'a>,
     {
         self.tests.insert(name, tests::BoxedTest::new(f));
@@ -573,7 +578,7 @@ impl<'source> Environment<'source> {
     /// functions and other global variables share the same namespace.
     pub fn add_function<F, Rv, Args>(&mut self, name: &'source str, f: F)
     where
-        Rv: Into<Value>,
+        Rv: FunctionResult,
         F: functions::Function<Rv, Args>,
         Args: for<'a> FunctionArgs<'a>,
     {
diff --git a/minijinja/src/filters.rs b/minijinja/src/filters.rs
index aa1de55..6cd133b 100644
--- a/minijinja/src/filters.rs
+++ b/minijinja/src/filters.rs
@@ -28,27 +28,38 @@
 //!
 //! # Custom Filters
 //!
-//! A custom filter is just a simple function which accepts inputs as parameters and then
-//! returns a new value.  For instance the following shows a filter which takes an input
-//! value and replaces whitespace with dashes and converts it to lowercase:
+//! A custom filter is just a simple function which accepts [`State`] and inputs
+//! as parameters and then returns a new value.  For instance the following
+//! shows a filter which takes an input value and replaces whitespace with
+//! dashes and converts it to lowercase:
 //!
 //! ```
-//! # use minijinja::{Environment, State, Error};
+//! # use minijinja::Environment;
 //! # let mut env = Environment::new();
-//! fn slugify(_state: &State, value: String) -> Result<String, Error> {
-//!     Ok(value.to_lowercase().split_whitespace().collect::<Vec<_>>().join("-"))
+//! use minijinja::State;
+//!
+//! fn slugify(_state: &State, value: String) -> String {
+//!     value.to_lowercase().split_whitespace().collect::<Vec<_>>().join("-")
 //! }
 //!
 //! env.add_filter("slugify", slugify);
 //! ```
 //!
-//! MiniJinja will perform the necessary conversions automatically via the
-//! [`FunctionArgs`](crate::value::FunctionArgs) and [`Into`] traits.
+//! MiniJinja will perform the necessary conversions automatically.  For more
+//! information see the [`Filter`] trait.
+//!
+//! # Built-in Filters
+//!
+//! When the `builtins` feature is enabled a range of built-in filters are
+//! automatically added to the environment.  These are also all provided in
+//! this module.  Note though that these functions are not to be
+//! called from Rust code as their exact interface (arguments and return types)
+//! might change from one MiniJinja version to another.
 use std::collections::BTreeMap;
 use std::sync::Arc;
 
 use crate::error::Error;
-use crate::value::{ArgType, FunctionArgs, Value};
+use crate::value::{ArgType, FunctionArgs, FunctionResult, Value};
 use crate::vm::State;
 use crate::AutoEscape;
 
@@ -58,19 +69,52 @@ type FilterFunc = dyn Fn(&State, &Value, &[Value]) -> Result<Value, Error> + Syn
 pub(crate) struct BoxedFilter(Arc<FilterFunc>);
 
 /// A utility trait that represents filters.
+///
+/// This trait is used by the [`add_filter`](crate::Environment::add_filter) method to abstract over
+/// different types of functions that implement filters.  Filters are functions
+/// which at the very least accept the [`State`] by reference as first parameter
+/// and the value that that the filter is applied to as second.  Additionally up to
+/// 4 further parameters are supported.
+///
+/// A filter can return any of the following types:
+///
+/// * `Rv` where `Rv` implements `Into<Value>`
+/// * `Result<Rv, Error>` where `Rv` implements `Into<Value>`
+///
+/// Filters accept one mandatory parameter which is the value the filter is
+/// applied to and up to 4 extra parameters.  The extra parameters can be
+/// marked optional by using `Option<T>`.  All types are supported for which
+/// [`ArgType`] is implemented.
+///
+/// ```
+/// # use minijinja::Environment;
+/// # let mut env = Environment::new();
+/// use minijinja::State;
+///
+/// fn slugify(_state: &State, value: String) -> String {
+///     value.to_lowercase().split_whitespace().collect::<Vec<_>>().join("-")
+/// }
+///
+/// env.add_filter("slugify", slugify);
+/// ```
+///
+/// For a list of built-in filters see [`filters`](crate::filters).
 pub trait Filter<V, Rv, Args>: Send + Sync + 'static {
     /// Applies a filter to value with the given arguments.
     #[doc(hidden)]
-    fn apply_to(&self, state: &State, value: V, args: Args) -> Result<Rv, Error>;
+    fn apply_to(&self, state: &State, value: V, args: Args) -> Rv;
 }
 
 macro_rules! tuple_impls {
     ( $( $name:ident )* ) => {
         impl<Func, V, Rv, $($name),*> Filter<V, Rv, ($($name,)*)> for Func
         where
-            Func: Fn(&State, V, $($name),*) -> Result<Rv, Error> + Send + Sync + 'static
+            Func: Fn(&State, V, $($name),*) -> Rv + Send + Sync + 'static,
+            V: for<'a> ArgType<'a>,
+            Rv: FunctionResult,
+            $($name: for<'a> ArgType<'a>),*
         {
-            fn apply_to(&self, state: &State, value: V, args: ($($name,)*)) -> Result<Rv, Error> {
+            fn apply_to(&self, state: &State, value: V, args: ($($name,)*)) -> Rv {
                 #[allow(non_snake_case)]
                 let ($($name,)*) = args;
                 (self)(state, value, $($name,)*)
@@ -91,7 +135,7 @@ impl BoxedFilter {
     where
         F: Filter<V, Rv, Args>,
         V: for<'a> ArgType<'a>,
-        Rv: Into<Value>,
+        Rv: FunctionResult,
         Args: for<'a> FunctionArgs<'a>,
     {
         BoxedFilter(Arc::new(
@@ -101,7 +145,7 @@ impl BoxedFilter {
                     ArgType::from_value(Some(value))?,
                     FunctionArgs::from_values(args)?,
                 )
-                .map(Into::into)
+                .into_result()
             },
         ))
     }
@@ -155,9 +199,9 @@ pub(crate) fn get_builtin_filters() -> BTreeMap<&'static str, BoxedFilter> {
 /// Marks a value as safe.  This converts it into a string.
 ///
 /// When a value is marked as safe, no further auto escaping will take place.
-pub fn safe(_state: &State, v: String) -> Result<Value, Error> {
+pub fn safe(_state: &State, v: String) -> Value {
     // TODO: this ideally understands which type of escaping is in use
-    Ok(Value::from_safe_string(v))
+    Value::from_safe_string(v)
 }
 
 /// Escapes a string.  By default to HTML.
@@ -205,8 +249,8 @@ mod builtins {
     /// <h1>{{ chapter.title|upper }}</h1>
     /// ```
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn upper(_state: &State, v: Value) -> Result<String, Error> {
-        Ok(v.to_cowstr().to_uppercase())
+    pub fn upper(_state: &State, v: Value) -> String {
+        v.to_cowstr().to_uppercase()
     }
 
     /// Converts a value to lowercase.
@@ -215,8 +259,8 @@ mod builtins {
     /// <h1>{{ chapter.title|lower }}</h1>
     /// ```
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn lower(_state: &State, v: Value) -> Result<String, Error> {
-        Ok(v.to_cowstr().to_lowercase())
+    pub fn lower(_state: &State, v: Value) -> String {
+        v.to_cowstr().to_lowercase()
     }
 
     /// Converts a value to title case.
@@ -225,7 +269,7 @@ mod builtins {
     /// <h1>{{ chapter.title|title }}</h1>
     /// ```
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn title(_state: &State, v: Value) -> Result<String, Error> {
+    pub fn title(_state: &State, v: Value) -> String {
         let mut rv = String::new();
         let mut capitalize = true;
         for c in v.to_cowstr().chars() {
@@ -239,7 +283,7 @@ mod builtins {
                 write!(rv, "{}", c.to_lowercase()).unwrap();
             }
         }
-        Ok(rv)
+        rv
     }
 
     /// Does a string replace.
@@ -251,9 +295,9 @@ mod builtins {
     ///   -> Goodbye World
     /// ```
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn replace(_state: &State, v: Value, from: Value, to: Value) -> Result<String, Error> {
-        Ok(v.to_cowstr()
-            .replace(&from.to_cowstr() as &str, &to.to_cowstr() as &str))
+    pub fn replace(_state: &State, v: Value, from: Value, to: Value) -> String {
+        v.to_cowstr()
+            .replace(&from.to_cowstr() as &str, &to.to_cowstr() as &str)
     }
 
     /// Returns the "length" of the value
@@ -264,8 +308,8 @@ mod builtins {
     /// <p>Search results: {{ results|length }}
     /// ```
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn length(_state: &State, v: Value) -> Result<Value, Error> {
-        v.len().map(Value::from).ok_or_else(|| {
+    pub fn length(_state: &State, v: Value) -> Result<usize, Error> {
+        v.len().ok_or_else(|| {
             Error::new(
                 ErrorKind::ImpossibleOperation,
                 format!("cannot calculate length of value of type {}", v.kind()),
@@ -317,7 +361,7 @@ mod builtins {
     pub fn items(_state: &State, v: Value) -> Result<Value, Error> {
         Ok(Value::from(
             match v.0 {
-                ValueRepr::Map(ref v) => v.iter().collect::<Vec<_>>(),
+                ValueRepr::Map(ref v) => v.iter(),
                 _ => {
                     return Err(Error::new(
                         ErrorKind::ImpossibleOperation,
@@ -325,7 +369,6 @@ mod builtins {
                     ))
                 }
             }
-            .into_iter()
             .map(|(k, v)| vec![Value::from(k.clone()), v.clone()])
             .collect::<Vec<_>>(),
         ))
@@ -356,13 +399,13 @@ mod builtins {
 
     /// Trims a value
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn trim(_state: &State, s: Value, chars: Option<Value>) -> Result<String, Error> {
+    pub fn trim(_state: &State, s: Value, chars: Option<Value>) -> String {
         match chars {
             Some(chars) => {
                 let chars = chars.to_cowstr().chars().collect::<Vec<_>>();
-                Ok(s.to_cowstr().trim_matches(&chars[..]).to_string())
+                s.to_cowstr().trim_matches(&chars[..]).to_string()
             }
-            None => Ok(s.to_cowstr().trim().to_string()),
+            None => s.to_cowstr().trim().to_string(),
         }
     }
 
@@ -412,12 +455,12 @@ mod builtins {
     /// <p>{{ my_variable|default("my_variable was not defined") }}</p>
     /// ```
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn default(_: &State, value: Value, other: Option<Value>) -> Result<Value, Error> {
-        Ok(if value.is_undefined() {
+    pub fn default(_: &State, value: Value, other: Option<Value>) -> Value {
+        if value.is_undefined() {
             other.unwrap_or_else(|| Value::from(""))
         } else {
             value
-        })
+        }
     }
 
     /// Returns the absolute value of a number.
@@ -547,8 +590,8 @@ mod builtins {
     /// This behaves the same as the if statement does with regards to
     /// handling of boolean values.
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn bool(_: &State, value: Value) -> Result<bool, Error> {
-        Ok(value.is_true())
+    pub fn bool(_: &State, value: Value) -> bool {
+        value.is_true()
     }
 
     /// Slice an iterable and return a list of lists containing
diff --git a/minijinja/src/functions.rs b/minijinja/src/functions.rs
index df6bf07..1f836ea 100644
--- a/minijinja/src/functions.rs
+++ b/minijinja/src/functions.rs
@@ -26,8 +26,10 @@
 //! render invocation.
 //!
 //! ```rust
-//! # use minijinja::{Environment, State, Error, ErrorKind};
+//! # use minijinja::Environment;
 //! # let mut env = Environment::new();
+//! use minijinja::{State, Error, ErrorKind};
+//!
 //! fn include_file(_state: &State, name: String) -> Result<String, Error> {
 //!     std::fs::read_to_string(&name)
 //!         .map_err(|e| Error::new(
@@ -38,12 +40,20 @@
 //!
 //! env.add_function("include_file", include_file);
 //! ```
+//!
+//! # Built-in Functions
+//!
+//! When the `builtins` feature is enabled a range of built-in functions are
+//! automatically added to the environment.  These are also all provided in
+//! this module.  Note though that these functions are not to be
+//! called from Rust code as their exact interface (arguments and return types)
+//! might change from one MiniJinja version to another.
 use std::collections::BTreeMap;
 use std::fmt;
 use std::sync::Arc;
 
 use crate::error::Error;
-use crate::value::{FunctionArgs, Object, Value};
+use crate::value::{ArgType, FunctionArgs, FunctionResult, Object, Value};
 use crate::vm::State;
 
 type FuncFunc = dyn Fn(&State, &[Value]) -> Result<Value, Error> + Sync + Send + 'static;
@@ -53,19 +63,54 @@ type FuncFunc = dyn Fn(&State, &[Value]) -> Result<Value, Error> + Sync + Send +
 pub(crate) struct BoxedFunction(Arc<FuncFunc>, &'static str);
 
 /// A utility trait that represents global functions.
+///
+/// This trait is used by the [`add_functino`](crate::Environment::add_function)
+/// method to abstract over different types of functions.
+///
+/// Functions which at the very least accept the [`State`] by reference as first
+/// parameter and additionally up to 4 further parameters.  They share much of
+/// their interface with [`filters`](crate::filters).
+///
+/// A function can return any of the following types:
+///
+/// * `Rv` where `Rv` implements `Into<Value>`
+/// * `Result<Rv, Error>` where `Rv` implements `Into<Value>`
+///
+/// The parameters can be marked optional by using `Option<T>`.  All types are
+/// supported for which [`ArgType`] is implemented.
+///
+/// ```rust
+/// # use minijinja::Environment;
+/// # let mut env = Environment::new();
+/// use minijinja::{State, Error, ErrorKind};
+///
+/// fn include_file(_state: &State, name: String) -> Result<String, Error> {
+///     std::fs::read_to_string(&name)
+///         .map_err(|e| Error::new(
+///             ErrorKind::ImpossibleOperation,
+///             "cannot load file"
+///         ).with_source(e))
+/// }
+///
+/// env.add_function("include_file", include_file);
+/// ```
+///
+/// For a list of built-in functions see [`functions`](crate::functions).
 pub trait Function<Rv, Args>: Send + Sync + 'static {
     /// Calls a function with the given arguments.
     #[doc(hidden)]
-    fn invoke(&self, env: &State, args: Args) -> Result<Rv, Error>;
+    fn invoke(&self, env: &State, args: Args) -> Rv;
 }
 
 macro_rules! tuple_impls {
     ( $( $name:ident )* ) => {
-        impl<F, Rv, $($name),*> Function<Rv, ($($name,)*)> for F
+        impl<Func, Rv, $($name),*> Function<Rv, ($($name,)*)> for Func
         where
-            F: Fn(&State, $($name),*) -> Result<Rv, Error> + Send + Sync + 'static
+            Func: Fn(&State, $($name),*) -> Rv + Send + Sync + 'static,
+            Rv: FunctionResult,
+            $($name: for<'a> ArgType<'a>),*
         {
-            fn invoke(&self, state: &State, args: ($($name,)*)) -> Result<Rv, Error> {
+            fn invoke(&self, state: &State, args: ($($name,)*)) -> Rv {
                 #[allow(non_snake_case)]
                 let ($($name,)*) = args;
                 (self)(state, $($name,)*)
@@ -85,13 +130,13 @@ impl BoxedFunction {
     pub fn new<F, Rv, Args>(f: F) -> BoxedFunction
     where
         F: Function<Rv, Args>,
-        Rv: Into<Value>,
+        Rv: FunctionResult,
         Args: for<'a> FunctionArgs<'a>,
     {
         BoxedFunction(
             Arc::new(move |env, args| -> Result<Value, Error> {
                 f.invoke(env, FunctionArgs::from_values(args)?)
-                    .map(Into::into)
+                    .into_result()
             }),
             std::any::type_name::<F>(),
         )
@@ -168,21 +213,16 @@ mod builtins {
     /// </ul>
     /// ```
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn range(
-        _state: &State,
-        lower: u32,
-        upper: Option<u32>,
-        step: Option<u32>,
-    ) -> Result<Vec<u32>, Error> {
+    pub fn range(_state: &State, lower: u32, upper: Option<u32>, step: Option<u32>) -> Vec<u32> {
         let rng = match upper {
             Some(upper) => lower..upper,
             None => 0..lower,
         };
-        Ok(if let Some(step) = step {
+        if let Some(step) = step {
             rng.step_by(step as usize).collect()
         } else {
             rng.collect()
-        })
+        }
     }
 
     /// Creates a dictionary.
@@ -218,8 +258,8 @@ mod builtins {
     /// <pre>{{ debug() }}</pre>
     /// ```
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn debug(state: &State) -> Result<String, Error> {
-        Ok(format!("{:#?}", state))
+    pub fn debug(state: &State) -> String {
+        format!("{:#?}", state)
     }
 }
 
diff --git a/minijinja/src/value/argtypes.rs b/minijinja/src/value/argtypes.rs
index d1d65a5..25bb647 100644
--- a/minijinja/src/value/argtypes.rs
+++ b/minijinja/src/value/argtypes.rs
@@ -6,16 +6,40 @@ use crate::error::{Error, ErrorKind};
 use crate::key::{Key, StaticKey};
 use crate::value::{Arc, Value, ValueRepr};
 
-/// Helper trait representing valid filter and test arguments.
+/// A utility trait that represents the return value of functions and filters.
+///
+/// It's implemented for the following types:
+///
+/// * `Rv` where `Rv` implements `Into<Value>`
+/// * `Result<Rv, Error>` where `Rv` implements `Into<Value>`
+///
+/// The equivalent for test functions is [`TestResult`](crate::tests::TestResult).
+pub trait FunctionResult {
+    #[doc(hidden)]
+    fn into_result(self) -> Result<Value, Error>;
+}
+
+impl<I: Into<Value>> FunctionResult for Result<I, Error> {
+    fn into_result(self) -> Result<Value, Error> {
+        self.map(Into::into)
+    }
+}
+
+impl<I: Into<Value>> FunctionResult for I {
+    fn into_result(self) -> Result<Value, Error> {
+        Ok(self.into())
+    }
+}
+
+/// Helper trait representing valid filter, test and function arguments.
 ///
 /// Since it's more convenient to write filters and tests with concrete
 /// types instead of values, this helper trait exists to automatically
 /// perform this conversion.  It is implemented for functions up to an
-/// arity of 5 parameters.
+/// arity of 4 parameters.
 ///
 /// For each argument the conversion is performed via the [`ArgType`]
-/// trait which is implemented for some primitive concrete types as well
-/// as these types wrapped in [`Option`].
+/// trait which is implemented for many common types.
 pub trait FunctionArgs<'a>: Sized {
     /// Converts to function arguments from a slice of values.
     fn from_values(values: &'a [Value]) -> Result<Self, Error>;
@@ -23,10 +47,20 @@ pub trait FunctionArgs<'a>: Sized {
 
 /// A trait implemented by all filter/test argument types.
 ///
-/// This trait is the companion to [`FunctionArgs`].  It's passed an
-/// `Option<Value>` where `Some` means the argument was provided or
-/// `None` if it was not.  This is used to implement optional arguments
-/// to functions.
+/// This trait is used by [`FunctionArgs`].  It's implemented for many common
+/// types that are typically passed to filters, tests or functions.  It's
+/// implemented for the following types:
+///
+/// * unsigned integers: [`u8`], [`u16`], [`u32`], [`u64`], [`u128`], [`usize`]
+/// * signed integers: [`i8`], [`i16`], [`i32`], [`i64`], [`i128`]
+/// * floats: [`f64`]
+/// * bool: [`bool`]
+/// * string: [`String`]
+/// * values: [`Value`]
+/// * vectors: [`Vec<T>`]
+///
+/// The type is also implemented for optional values (`Value<T>`) which is used
+/// to encode optional parameters to filters, functions or tests.
 pub trait ArgType<'a>: Sized {
     #[doc(hidden)]
     fn from_value(value: Option<&'a Value>) -> Result<Self, Error>;
@@ -34,7 +68,9 @@ pub trait ArgType<'a>: Sized {
 
 macro_rules! tuple_impls {
     ( $( $name:ident )* ) => {
-        impl<'a, $($name: ArgType<'a>,)*> FunctionArgs<'a> for ($($name,)*) {
+        impl<'a, $($name),*> FunctionArgs<'a> for ($($name,)*)
+            where $($name: ArgType<'a>,)*
+        {
             fn from_values(values: &'a [Value]) -> Result<Self, Error> {
                 #![allow(non_snake_case, unused)]
                 let arg_count = 0 $(
diff --git a/minijinja/src/value/mod.rs b/minijinja/src/value/mod.rs
index 10a9cac..be3bca1 100644
--- a/minijinja/src/value/mod.rs
+++ b/minijinja/src/value/mod.rs
@@ -83,7 +83,7 @@ use crate::utils::OnDrop;
 use crate::value::serialize::ValueSerializer;
 use crate::vm::State;
 
-pub use crate::value::argtypes::{ArgType, FunctionArgs};
+pub use crate::value::argtypes::{ArgType, FunctionArgs, FunctionResult};
 pub use crate::value::object::Object;
 
 mod argtypes;


Test patch:
diff --git a/minijinja/src/tests.rs b/minijinja/src/tests.rs
index f0a135d..f71e364 100644
--- a/minijinja/src/tests.rs
+++ b/minijinja/src/tests.rs
@@ -1,9 +1,9 @@
 //! Test functions and abstractions.
 //!
-//! Test functions in MiniJinja are like (filters)[crate::filters] but a different syntax
-//! is used to invoke them and they have to return boolean values.  For instance the
-//! expression `{% if foo is odd %}` invokes the [`is_odd`] test to check if the value
-//! is indeed an odd number.
+//! Test functions in MiniJinja are like [`filters`](crate::filters) but a
+//! different syntax is used to invoke them and they have to return boolean
+//! values.  For instance the expression `{% if foo is odd %}` invokes the
+//! [`is_odd`] test to check if the value is indeed an odd number.
 //!
 //! MiniJinja comes with some built-in test functions that are listed below. To
 //! create a custom test write a function that takes at least a
@@ -28,23 +28,33 @@
 //!
 //! # Custom Tests
 //!
-//! A custom test function is just a simple function which accepts inputs as
-//! parameters and then returns a bool wrapped in a result. For instance the
-//! following shows a test function which takes an input value and checks if
-//! it's lowercase:
+//! A custom test function is just a simple function which accepts [`State`] and
+//! inputs as parameters and then returns a bool. For instance the following
+//! shows a test function which takes an input value and checks if it's
+//! lowercase:
 //!
 //! ```
-//! # use minijinja::{State, Environment, Error};
+//! # use minijinja::Environment;
 //! # let mut env = Environment::new();
-//! fn is_lowercase(_state: &State, value: String) -> Result<bool, Error> {
-//!    Ok(value.chars().all(|x| x.is_lowercase()))
+//! use minijinja::State;
+//!
+//! fn is_lowercase(_state: &State, value: String) -> bool {
+//!    value.chars().all(|x| x.is_lowercase())
 //! }
 //!
 //! env.add_test("lowercase", is_lowercase);
 //! ```
 //!
-//! MiniJinja will perform the necessary conversions automatically via the
-//! [`FunctionArgs`](crate::value::FunctionArgs) trait.
+//! MiniJinja will perform the necessary conversions automatically.  For more
+//! information see the [`Test`] trait.
+//!
+//! # Built-in Tests
+//!
+//! When the `builtins` feature is enabled a range of built-in tests are
+//! automatically added to the environment.  These are also all provided in
+//! this module.  Note though that these functions are not to be
+//! called from Rust code as their exact interface (arguments and return types)
+//! might change from one MiniJinja version to another.
 use std::collections::BTreeMap;
 use std::sync::Arc;
 
@@ -57,20 +67,79 @@ type TestFunc = dyn Fn(&State, &Value, &[Value]) -> Result<bool, Error> + Sync +
 #[derive(Clone)]
 pub(crate) struct BoxedTest(Arc<TestFunc>);
 
-/// A utility trait that represents filters.
-pub trait Test<V, Args>: Send + Sync + 'static {
+/// A utility trait that represents the return value of filters.
+///
+/// It's implemented for the following types:
+///
+/// * `bool`
+/// * `Result<bool, Error>`
+///
+/// The equivalent for filters or functions is [`FunctionResult`](crate::value::FunctionResult).
+pub trait TestResult {
+    #[doc(hidden)]
+    fn into_result(self) -> Result<bool, Error>;
+}
+
+impl TestResult for Result<bool, Error> {
+    fn into_result(self) -> Result<bool, Error> {
+        self
+    }
+}
+
+impl TestResult for bool {
+    fn into_result(self) -> Result<bool, Error> {
+        Ok(self)
+    }
+}
+
+/// A utility trait that represents test functions.
+///
+/// This trait is used by the [`add_test`](crate::Environment::add_test) method to abstract over
+/// different types of functions that implement tests.  Tests are similar to
+/// [`filters`](crate::filters) but they always return boolean values and use a
+/// slightly different syntax to filters.  Like filters they accept the [`State`] by
+/// reference as first parameter and the value that that the test is applied to as second.
+/// Additionally up to 4 further parameters are supported.
+///
+/// A test function can return any of the following types:
+///
+/// * `bool`
+/// * `Result<bool, Error>`
+///
+/// Tests accept one mandatory parameter which is the value the filter is
+/// applied to and up to 4 extra parameters.  The extra parameters can be
+/// marked optional by using `Option<T>`.  All types are supported for which
+/// [`ArgType`] is implemented.
+///
+/// ```
+/// # use minijinja::Environment;
+/// # let mut env = Environment::new();
+/// use minijinja::State;
+///
+/// fn is_lowercase(_state: &State, value: String) -> bool {
+///    value.chars().all(|x| x.is_lowercase())
+/// }
+///
+/// env.add_test("lowercase", is_lowercase);
+/// ```
+///
+/// For a list of built-in tests see [`tests`](crate::tests).
+pub trait Test<V, Rv, Args>: Send + Sync + 'static {
     /// Performs a test to value with the given arguments.
     #[doc(hidden)]
-    fn perform(&self, state: &State, value: V, args: Args) -> Result<bool, Error>;
+    fn perform(&self, state: &State, value: V, args: Args) -> Rv;
 }
 
 macro_rules! tuple_impls {
     ( $( $name:ident )* ) => {
-        impl<Func, V, $($name),*> Test<V, ($($name,)*)> for Func
+        impl<Func, V, Rv, $($name),*> Test<V, Rv, ($($name,)*)> for Func
         where
-            Func: Fn(&State, V, $($name),*) -> Result<bool, Error> + Send + Sync + 'static
+            Func: Fn(&State, V, $($name),*) -> Rv + Send + Sync + 'static,
+            V: for<'a> ArgType<'a>,
+            Rv: TestResult,
+            $($name: for<'a> ArgType<'a>),*
         {
-            fn perform(&self, state: &State, value: V, args: ($($name,)*)) -> Result<bool, Error> {
+            fn perform(&self, state: &State, value: V, args: ($($name,)*)) -> Rv {
                 #[allow(non_snake_case)]
                 let ($($name,)*) = args;
                 (self)(state, value, $($name,)*)
@@ -87,10 +156,11 @@ tuple_impls! { A B C D }
 
 impl BoxedTest {
     /// Creates a new boxed filter.
-    pub fn new<F, V, Args>(f: F) -> BoxedTest
+    pub fn new<F, V, Rv, Args>(f: F) -> BoxedTest
     where
-        F: Test<V, Args>,
+        F: Test<V, Rv, Args>,
         V: for<'a> ArgType<'a>,
+        Rv: TestResult,
         Args: for<'a> FunctionArgs<'a>,
     {
         BoxedTest(Arc::new(move |state, value, args| -> Result<bool, Error> {
@@ -100,6 +170,7 @@ impl BoxedTest {
                 ArgType::from_value(value)?,
                 FunctionArgs::from_values(args)?,
             )
+            .into_result()
         }))
     }
 
@@ -138,68 +209,68 @@ mod builtins {
 
     /// Checks if a value is odd.
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn is_odd(_state: &State, v: Value) -> Result<bool, Error> {
-        Ok(i128::try_from(v).ok().map_or(false, |x| x % 2 != 0))
+    pub fn is_odd(_state: &State, v: Value) -> bool {
+        i128::try_from(v).ok().map_or(false, |x| x % 2 != 0)
     }
 
     /// Checks if a value is even.
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn is_even(_state: &State, v: Value) -> Result<bool, Error> {
-        Ok(i128::try_from(v).ok().map_or(false, |x| x % 2 == 0))
+    pub fn is_even(_state: &State, v: Value) -> bool {
+        i128::try_from(v).ok().map_or(false, |x| x % 2 == 0)
     }
 
     /// Checks if a value is undefined.
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn is_undefined(_state: &State, v: Value) -> Result<bool, Error> {
-        Ok(v.is_undefined())
+    pub fn is_undefined(_state: &State, v: Value) -> bool {
+        v.is_undefined()
     }
 
     /// Checks if a value is defined.
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn is_defined(_state: &State, v: Value) -> Result<bool, Error> {
-        Ok(!v.is_undefined())
+    pub fn is_defined(_state: &State, v: Value) -> bool {
+        !v.is_undefined()
     }
 
     /// Checks if this value is a number.
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn is_number(_state: &State, v: Value) -> Result<bool, Error> {
-        Ok(matches!(v.kind(), ValueKind::Number))
+    pub fn is_number(_state: &State, v: Value) -> bool {
+        matches!(v.kind(), ValueKind::Number)
     }
 
     /// Checks if this value is a string.
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn is_string(_state: &State, v: Value) -> Result<bool, Error> {
-        Ok(matches!(v.kind(), ValueKind::String))
+    pub fn is_string(_state: &State, v: Value) -> bool {
+        matches!(v.kind(), ValueKind::String)
     }
 
     /// Checks if this value is a sequence
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn is_sequence(_state: &State, v: Value) -> Result<bool, Error> {
-        Ok(matches!(v.kind(), ValueKind::Seq))
+    pub fn is_sequence(_state: &State, v: Value) -> bool {
+        matches!(v.kind(), ValueKind::Seq)
     }
 
     /// Checks if this value is a mapping
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn is_mapping(_state: &State, v: Value) -> Result<bool, Error> {
-        Ok(matches!(v.kind(), ValueKind::Map))
+    pub fn is_mapping(_state: &State, v: Value) -> bool {
+        matches!(v.kind(), ValueKind::Map)
     }
 
     /// Checks if the value is starting with a string.
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn is_startingwith(_state: &State, v: String, other: String) -> Result<bool, Error> {
-        Ok(v.starts_with(&other))
+    pub fn is_startingwith(_state: &State, v: String, other: String) -> bool {
+        v.starts_with(&other)
     }
 
     /// Checks if the value is ending with a string.
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn is_endingwith(_state: &State, v: String, other: String) -> Result<bool, Error> {
-        Ok(v.ends_with(&other))
+    pub fn is_endingwith(_state: &State, v: String, other: String) -> bool {
+        v.ends_with(&other)
     }
 
     #[test]
     fn test_basics() {
-        fn test(_: &State, a: u32, b: u32) -> Result<bool, Error> {
-            Ok(a == b)
+        fn test(_: &State, a: u32, b: u32) -> bool {
+            a == b
         }
 
         let env = crate::Environment::new();