Repository: mitsuhiko/minijinja
Pull request #114 (2022-09-09 21:42:05)

Added support for remaining arguments
This now lets a user use remaining arguments on a filter.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9c9b9db..2ade29d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ All notable changes to MiniJinja are documented here.
 - Added support for rendering to `io::Write`.  (#111)
 - Make it impossible to implement `Fitler`, `Test`
   or `Function` from outside the crate (sealed the traits).
+- Added support for remaining arguments with `Rest`.
 
 # 0.20.0
 
diff --git a/minijinja/src/filters.rs b/minijinja/src/filters.rs
index f034a92..94ef273 100644
--- a/minijinja/src/filters.rs
+++ b/minijinja/src/filters.rs
@@ -85,8 +85,13 @@ pub(crate) struct BoxedFilter(Arc<FilterFunc>);
 ///
 /// 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.
+/// marked optional by using `Option<T>`.  The last argument can also use
+/// [`Rest<T>`](crate::value::Rest) to capture the remaining arguments.  All
+/// types are supported for which [`ArgType`] is implemented.
+///
+/// For a list of built-in filters see [`filters`](crate::filters).
+///
+/// # Basic Example
 ///
 /// ```
 /// # use minijinja::Environment;
@@ -100,7 +105,48 @@ pub(crate) struct BoxedFilter(Arc<FilterFunc>);
 /// env.add_filter("slugify", slugify);
 /// ```
 ///
-/// For a list of built-in filters see [`filters`](crate::filters).
+/// ```jinja
+/// {{ "Foo Bar Baz"|slugify }} -> foo-bar-baz
+/// ```
+///
+/// # Arguments and Optional Arguments
+///
+/// ```
+/// # use minijinja::Environment;
+/// # let mut env = Environment::new();
+/// use minijinja::State;
+///
+/// fn substr(_state: &State, value: String, start: u32, end: Option<u32>) -> String {
+///     let end = end.unwrap_or(value.len() as _);
+///     value.get(start as usize..end as usize).unwrap_or_default().into()
+/// }
+///
+/// env.add_filter("substr", substr);
+/// ```
+///
+/// ```jinja
+/// {{ "Foo Bar Baz"|substr(4) }} -> Bar Baz
+/// {{ "Foo Bar Baz"|substr(4, 7) }} -> Bar
+/// ```
+///
+/// # Variadic
+///
+/// ```
+/// # use minijinja::Environment;
+/// # let mut env = Environment::new();
+/// use minijinja::State;
+/// use minijinja::value::Rest;
+///
+/// fn pyjoin(_state: &State, joiner: String, values: Rest<String>) -> String {
+///     values.connect(&joiner)
+/// }
+///
+/// env.add_filter("pyjoin", pyjoin);
+/// ```
+///
+/// ```jinja
+/// {{ "|".join(1, 2, 3) }} -> 1|2|3
+/// ```
 pub trait Filter<V, Rv, Args>: Send + Sync + 'static {
     /// Applies a filter to value with the given arguments.
     #[doc(hidden)]
@@ -766,6 +812,41 @@ mod builtins {
         });
     }
 
+    #[test]
+    fn test_rest_args() {
+        fn sum(_: &State, val: u32, rest: crate::value::Rest<u32>) -> u32 {
+            rest.iter().fold(val, |a, b| a + b)
+        }
+
+        let env = crate::Environment::new();
+        State::with_dummy(&env, |state| {
+            let bx = BoxedFilter::new(sum);
+            assert_eq!(
+                bx.apply_to(
+                    state,
+                    &Value::from(1),
+                    &[Value::from(2), Value::from(3), Value::from(4)][..]
+                )
+                .unwrap(),
+                Value::from(1 + 2 + 3 + 4)
+            );
+        });
+    }
+
+    #[test]
+    #[should_panic = "cannot collect remaining arguments in this argument position"]
+    fn test_incorrect_rest_args() {
+        fn sum(_: &State, _: crate::value::Rest<u32>) -> u32 {
+            panic!("should never happen");
+        }
+
+        let env = crate::Environment::new();
+        State::with_dummy(&env, |state| {
+            let bx = BoxedFilter::new(sum);
+            bx.apply_to(state, &Value::from(1), &[]).unwrap();
+        });
+    }
+
     #[test]
     fn test_optional_args() {
         fn add(_: &State, val: u32, a: u32, b: Option<u32>) -> Result<u32, Error> {
diff --git a/minijinja/src/functions.rs b/minijinja/src/functions.rs
index bc9779f..897a0e7 100644
--- a/minijinja/src/functions.rs
+++ b/minijinja/src/functions.rs
@@ -76,8 +76,14 @@ pub(crate) struct BoxedFunction(Arc<FuncFunc>, &'static str);
 /// * `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.
+/// The parameters can be marked optional by using `Option<T>`.  The last
+/// argument can also use [`Rest<T>`](crate::value::Rest) to capture the
+/// remaining arguments.  All types are supported for which [`ArgType`] is
+/// implemented.
+///
+/// For a list of built-in functions see [`functions`](crate::functions).
+///
+/// # Basic Example
 ///
 /// ```rust
 /// # use minijinja::Environment;
@@ -95,7 +101,28 @@ pub(crate) struct BoxedFunction(Arc<FuncFunc>, &'static str);
 /// env.add_function("include_file", include_file);
 /// ```
 ///
-/// For a list of built-in functions see [`functions`](crate::functions).
+/// ```jinja
+/// {{ include_file("filname.txt") }}
+/// ```
+///
+/// # Variadic
+///
+/// ```
+/// # use minijinja::Environment;
+/// # let mut env = Environment::new();
+/// use minijinja::State;
+/// use minijinja::value::Rest;
+///
+/// fn sum(_state: &State, values: Rest<i64>) -> i64 {
+///     values.iter().sum()
+/// }
+///
+/// env.add_function("sum", sum);
+/// ```
+///
+/// ```jinja
+/// {{ sum(1, 2, 3) }} -> 6
+/// ```
 pub trait Function<Rv, Args>: Send + Sync + 'static {
     /// Calls a function with the given arguments.
     #[doc(hidden)]
diff --git a/minijinja/src/value/argtypes.rs b/minijinja/src/value/argtypes.rs
index 25bb647..84244dd 100644
--- a/minijinja/src/value/argtypes.rs
+++ b/minijinja/src/value/argtypes.rs
@@ -1,6 +1,7 @@
 use std::borrow::Cow;
 use std::collections::BTreeMap;
 use std::convert::TryFrom;
+use std::ops::{Deref, DerefMut};
 
 use crate::error::{Error, ErrorKind};
 use crate::key::{Key, StaticKey};
@@ -59,15 +60,23 @@ pub trait FunctionArgs<'a>: Sized {
 /// * 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.
+/// The type is also implemented for optional values (`Option<T>`) which is used
+/// to encode optional parameters to filters, functions or tests.  Additionally
+/// it's implemented for [`Rest<T>`] which is used to encode the remaining arguments
+/// of a function call.
 pub trait ArgType<'a>: Sized {
     #[doc(hidden)]
     fn from_value(value: Option<&'a Value>) -> Result<Self, Error>;
+
+    #[doc(hidden)]
+    #[inline(always)]
+    fn from_rest_values(_values: &'a [Value]) -> Result<Option<Self>, Error> {
+        Ok(None)
+    }
 }
 
 macro_rules! tuple_impls {
-    ( $( $name:ident )* ) => {
+    ( $( $name:ident )* $(; ( $($alt_name:ident)* ) $rest_name:ident)? ) => {
         impl<'a, $($name),*> FunctionArgs<'a> for ($($name,)*)
             where $($name: ArgType<'a>,)*
         {
@@ -76,6 +85,19 @@ macro_rules! tuple_impls {
                 let arg_count = 0 $(
                     + { let $name = (); 1 }
                 )*;
+
+                $(
+                    let rest_values = values.get(arg_count - 1..).unwrap_or_default();
+                    if let Some(rest) = $rest_name::from_rest_values(rest_values)? {
+                        let mut idx = 0;
+                        $(
+                            let $alt_name = ArgType::from_value(values.get(idx))?;
+                            idx += 1;
+                        )*
+                        return Ok(( $($alt_name,)* rest ,));
+                    }
+                )?
+
                 if values.len() > arg_count {
                     return Err(Error::new(
                         ErrorKind::InvalidArguments,
@@ -96,10 +118,10 @@ macro_rules! tuple_impls {
 }
 
 tuple_impls! {}
-tuple_impls! { A }
-tuple_impls! { A B }
-tuple_impls! { A B C }
-tuple_impls! { A B C D }
+tuple_impls! { A; () A }
+tuple_impls! { A B; (A) B }
+tuple_impls! { A B C; (A B) C }
+tuple_impls! { A B C D; (A B C) D }
 
 impl From<ValueRepr> for Value {
     #[inline(always)]
@@ -293,6 +315,61 @@ primitive_try_from!(f64, {
     ValueRepr::F64(val) => val,
 });
 
+/// Utility type to capture remaining arguments.
+///
+/// In some cases you might want to have a variadic function.  In that case
+/// you can define the last argument to a [`Filter`](crate::filters::Filter),
+/// [`Test`](crate::tests::Test) or [`Function`](crate::functions::Function)
+/// this way.  The `Rest<T>` type will collect all the remaining arguments
+/// here.  It's implemented for all [`ArgType`]s.  The type itself deref's
+/// into the inner vector.
+///
+/// ```
+/// # use minijinja::Environment;
+/// # let mut env = Environment::new();
+/// use minijinja::State;
+/// use minijinja::value::Rest;
+///
+/// fn sum(_state: &State, values: Rest<i64>) -> i64 {
+///     values.iter().sum()
+/// }
+/// ```
+#[derive(Debug)]
+pub struct Rest<T>(pub Vec<T>);
+
+impl<T> Deref for Rest<T> {
+    type Target = Vec<T>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl<T> DerefMut for Rest<T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl<'a, T: ArgType<'a>> ArgType<'a> for Rest<T> {
+    fn from_value(_value: Option<&'a Value>) -> Result<Self, Error> {
+        Err(Error::new(
+            ErrorKind::ImpossibleOperation,
+            "cannot collect remaining arguments in this argument position",
+        ))
+    }
+
+    #[inline(always)]
+    fn from_rest_values(values: &'a [Value]) -> Result<Option<Self>, Error> {
+        Ok(Some(Rest(
+            values
+                .iter()
+                .map(|v| ArgType::from_value(Some(v)))
+                .collect::<Result<_, _>>()?,
+        )))
+    }
+}
+
 impl<'a> ArgType<'a> for Value {
     fn from_value(value: Option<&'a Value>) -> Result<Self, Error> {
         match value {
diff --git a/minijinja/src/value/mod.rs b/minijinja/src/value/mod.rs
index b6acbea..cb24cb3 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, FunctionResult};
+pub use crate::value::argtypes::{ArgType, FunctionArgs, FunctionResult, Rest};
 pub use crate::value::object::Object;
 
 mod argtypes;


Test patch:
diff --git a/minijinja/src/tests.rs b/minijinja/src/tests.rs
index b91b9f6..b018876 100644
--- a/minijinja/src/tests.rs
+++ b/minijinja/src/tests.rs
@@ -108,8 +108,13 @@ impl TestResult for bool {
 ///
 /// 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.
+/// marked optional by using `Option<T>`.  The last argument can also use
+/// [`Rest<T>`](crate::value::Rest) to capture the remaining arguments.  All
+/// types are supported for which [`ArgType`] is implemented.
+///
+/// For a list of built-in tests see [`tests`](crate::tests).
+///
+/// # Basic Example
 ///
 /// ```
 /// # use minijinja::Environment;
@@ -123,7 +128,27 @@ impl TestResult for bool {
 /// env.add_test("lowercase", is_lowercase);
 /// ```
 ///
-/// For a list of built-in tests see [`tests`](crate::tests).
+/// ```jinja
+/// {{ "foo" is lowercase }} -> true
+/// ```
+///
+/// # Arguments and Optional Arguments
+///
+/// ```
+/// # use minijinja::Environment;
+/// # let mut env = Environment::new();
+/// use minijinja::State;
+///
+/// fn is_containing(_state: &State, value: String, other: String) -> bool {
+///     value.contains(&other)
+/// }
+///
+/// env.add_test("containing", is_containing);
+/// ```
+///
+/// ```jinja
+/// {{ "foo" is containing("o") }} -> true
+/// ```
 pub trait Test<V, Rv, Args>: Send + Sync + 'static {
     /// Performs a test to value with the given arguments.
     #[doc(hidden)]