Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for remaining arguments #114

Merged
merged 1 commit into from Sep 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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