diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c9b9db2..2ade29d8 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 f034a92b..94ef273d 100644 --- a/minijinja/src/filters.rs +++ b/minijinja/src/filters.rs @@ -85,8 +85,13 @@ pub(crate) struct BoxedFilter(Arc); /// /// 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`. All types are supported for which -/// [`ArgType`] is implemented. +/// marked optional by using `Option`. The last argument can also use +/// [`Rest`](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); /// 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) -> 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 { +/// values.connect(&joiner) +/// } +/// +/// env.add_filter("pyjoin", pyjoin); +/// ``` +/// +/// ```jinja +/// {{ "|".join(1, 2, 3) }} -> 1|2|3 +/// ``` pub trait Filter: 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 { + 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 { + 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) -> Result { diff --git a/minijinja/src/functions.rs b/minijinja/src/functions.rs index bc9779f3..897a0e7c 100644 --- a/minijinja/src/functions.rs +++ b/minijinja/src/functions.rs @@ -76,8 +76,14 @@ pub(crate) struct BoxedFunction(Arc, &'static str); /// * `Rv` where `Rv` implements `Into` /// * `Result` where `Rv` implements `Into` /// -/// The parameters can be marked optional by using `Option`. All types are -/// supported for which [`ArgType`] is implemented. +/// The parameters can be marked optional by using `Option`. The last +/// argument can also use [`Rest`](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, &'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 { +/// values.iter().sum() +/// } +/// +/// env.add_function("sum", sum); +/// ``` +/// +/// ```jinja +/// {{ sum(1, 2, 3) }} -> 6 +/// ``` pub trait Function: Send + Sync + 'static { /// Calls a function with the given arguments. #[doc(hidden)] diff --git a/minijinja/src/tests.rs b/minijinja/src/tests.rs index b91b9f6e..b018876d 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`. All types are supported for which -/// [`ArgType`] is implemented. +/// marked optional by using `Option`. The last argument can also use +/// [`Rest`](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: Send + Sync + 'static { /// Performs a test to value with the given arguments. #[doc(hidden)] diff --git a/minijinja/src/value/argtypes.rs b/minijinja/src/value/argtypes.rs index 25bb6474..84244dd2 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`] /// -/// The type is also implemented for optional values (`Value`) which is used -/// to encode optional parameters to filters, functions or tests. +/// The type is also implemented for optional values (`Option`) which is used +/// to encode optional parameters to filters, functions or tests. Additionally +/// it's implemented for [`Rest`] 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; + + #[doc(hidden)] + #[inline(always)] + fn from_rest_values(_values: &'a [Value]) -> Result, 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 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` 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 { +/// values.iter().sum() +/// } +/// ``` +#[derive(Debug)] +pub struct Rest(pub Vec); + +impl Deref for Rest { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Rest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'a, T: ArgType<'a>> ArgType<'a> for Rest { + fn from_value(_value: Option<&'a Value>) -> Result { + Err(Error::new( + ErrorKind::ImpossibleOperation, + "cannot collect remaining arguments in this argument position", + )) + } + + #[inline(always)] + fn from_rest_values(values: &'a [Value]) -> Result, Error> { + Ok(Some(Rest( + values + .iter() + .map(|v| ArgType::from_value(Some(v))) + .collect::>()?, + ))) + } +} + impl<'a> ArgType<'a> for Value { fn from_value(value: Option<&'a Value>) -> Result { match value { diff --git a/minijinja/src/value/mod.rs b/minijinja/src/value/mod.rs index b6acbeaf..cb24cb3c 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;