Skip to content

Commit

Permalink
Added support for remaining arguments (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Sep 9, 2022
1 parent 0776959 commit eddfd1d
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
87 changes: 84 additions & 3 deletions minijinja/src/filters.rs
Expand Up @@ -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;
Expand All @@ -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)]
Expand Down Expand Up @@ -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> {
Expand Down
33 changes: 30 additions & 3 deletions minijinja/src/functions.rs
Expand Up @@ -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;
Expand All @@ -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)]
Expand Down
31 changes: 28 additions & 3 deletions minijinja/src/tests.rs
Expand Up @@ -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;
Expand All @@ -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)]
Expand Down
91 changes: 84 additions & 7 deletions 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};
Expand Down Expand Up @@ -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>,)*
{
Expand All @@ -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,
Expand All @@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion minijinja/src/value/mod.rs
Expand Up @@ -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;
Expand Down

0 comments on commit eddfd1d

Please sign in to comment.