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

feat: support keyword arguments with no definition on custom filters #516

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/core/src/parser/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub struct ParameterReflection {
pub trait FilterParametersReflection {
fn positional_parameters() -> &'static [ParameterReflection];
fn keyword_parameters() -> &'static [ParameterReflection];
fn keyword_group_parameters() -> &'static [ParameterReflection];
}

/// A trait that holds the information of a filter about itself, such as
Expand All @@ -42,6 +43,7 @@ pub trait FilterReflection {

fn positional_parameters(&self) -> &'static [ParameterReflection];
fn keyword_parameters(&self) -> &'static [ParameterReflection];
fn keyword_group_parameters(&self) -> &'static [ParameterReflection];
}

/// A trait that declares and holds the parameters of a filter.
Expand Down
32 changes: 32 additions & 0 deletions crates/core/src/runtime/expression.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::fmt;

use crate::error::Result;
use crate::model::Object;
use crate::model::Scalar;
use crate::model::Value;
use crate::model::ValueCow;
Expand All @@ -9,26 +10,39 @@ use crate::model::ValueView;
use super::variable::Variable;
use super::Runtime;

use std::collections::HashMap;

/// An un-evaluated `Value`.
#[derive(Debug, Clone, PartialEq)]
pub enum Expression {
/// Un-evaluated.
Variable(Variable),
/// Evaluated.
Literal(Value),
/// Used for evaluating object literals,
ObjectLiteral(ObjectLiteral),
}

type ObjectLiteral = HashMap<String, Expression>;

impl Expression {
/// Create an expression from a scalar literal.
pub fn with_literal<S: Into<Scalar>>(literal: S) -> Self {
Expression::Literal(Value::scalar(literal))
}

/// Creates an expression from an object literal (used when parsing filter
/// arguments)
pub fn with_object_literal(object_literal_expr: ObjectLiteral) -> Self {
Expression::ObjectLiteral(object_literal_expr)
}

/// Convert into a literal if possible.
pub fn into_literal(self) -> Option<Value> {
match self {
Expression::Literal(x) => Some(x),
Expression::Variable(_) => None,
Expression::ObjectLiteral(_) => None,
}
}

Expand All @@ -37,6 +51,7 @@ impl Expression {
match self {
Expression::Literal(_) => None,
Expression::Variable(x) => Some(x),
Expression::ObjectLiteral(_) => None,
}
}

Expand All @@ -48,6 +63,16 @@ impl Expression {
let path = x.try_evaluate(runtime)?;
runtime.try_get(&path)
}
Expression::ObjectLiteral(ref obj_lit) => {
let obj = obj_lit
.iter()
.map(|(key, expr)| match expr.try_evaluate(runtime) {
Some(result) => (key.into(), result.to_value()),
None => (key.into(), Value::Nil),
})
.collect::<Object>();
Some(ValueCow::Owned(obj.to_value()))
}
}
}

Expand All @@ -57,8 +82,14 @@ impl Expression {
Expression::Literal(ref x) => ValueCow::Borrowed(x),
Expression::Variable(ref x) => {
let path = x.evaluate(runtime)?;

runtime.get(&path)?
}
Expression::ObjectLiteral(obj_lit) => obj_lit
.iter()
.map(|(key, expr)| (key.into(), expr.evaluate(runtime).unwrap().to_value()))
.collect::<Object>()
.into(),
};
Ok(val)
}
Expand All @@ -69,6 +100,7 @@ impl fmt::Display for Expression {
match self {
Expression::Literal(ref x) => write!(f, "{}", x.source()),
Expression::Variable(ref x) => write!(f, "{}", x),
Expression::ObjectLiteral(ref x) => write!(f, "{:?}", x),
}
}
}
117 changes: 107 additions & 10 deletions crates/derive/src/filter_parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ impl<'a> FilterParameters<'a> {
));
}

if fields.more_than_one_keyword_group_parameter() {
let grouped_keyword_fields = fields
.parameters
.iter()
.filter(|parameter| parameter.is_keyword_group())
.collect::<Vec<_>>();

return Err(Error::new_spanned(
grouped_keyword_fields.first(),
"Found more than one keyword_group parameter, this is not allowed.",
));
}

let name = ident;
let evaluated_name = Self::parse_attrs(attrs)?
.unwrap_or_else(|| Ident::new(&format!("Evaluated{}", name), Span::call_site()));
Expand Down Expand Up @@ -115,6 +128,16 @@ impl<'a> FilterParametersFields<'a> {
.find(|parameter| !parameter.is_optional())
}

/// Predicate that indicates the presence of more than one keyword group
/// argument
fn more_than_one_keyword_group_parameter(&self) -> bool {
self.parameters
.iter()
.filter(|parameter| parameter.is_keyword_group())
.count()
> 1
}

/// Tries to create a new `FilterParametersFields` from the given `Fields`
fn from_fields(fields: &'a Fields) -> Result<Self> {
match fields {
Expand Down Expand Up @@ -256,6 +279,11 @@ impl<'a> FilterParameter<'a> {
self.meta.mode == FilterParameterMode::Keyword
}

/// Returns whether this is a keyword list field.
fn is_keyword_group(&self) -> bool {
self.meta.mode == FilterParameterMode::KeywordGroup
}

/// Returns the name of this parameter in liquid.
///
/// That is, by default, the name of the field as a string. However,
Expand All @@ -279,13 +307,15 @@ impl<'a> ToTokens for FilterParameter<'a> {
enum FilterParameterMode {
Keyword,
Positional,
KeywordGroup,
}

impl FromStr for FilterParameterMode {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"keyword" => Ok(FilterParameterMode::Keyword),
"keyword_group" => Ok(FilterParameterMode::KeywordGroup),
"positional" => Ok(FilterParameterMode::Positional),
s => Err(format!(
"Expected either \"keyword\" or \"positional\". Found \"{}\".",
Expand Down Expand Up @@ -424,6 +454,15 @@ fn generate_construct_positional_field(
}
}

/// Generates the statement that assigns the keyword list argument.
fn generate_construct_keyword_group_field(field: &FilterParameter<'_>) -> TokenStream {
let name = &field.name;

quote! {
let #name = Expression::with_object_literal(keyword_as_map);
}
}

/// Generates the statement that evaluates the `Expression`
fn generate_evaluate_field(field: &FilterParameter<'_>) -> TokenStream {
let name = &field.name;
Expand Down Expand Up @@ -582,6 +621,13 @@ fn generate_impl_filter_parameters(filter_parameters: &FilterParameters<'_>) ->
.iter()
.filter(|parameter| parameter.is_keyword());

let keyword_group_fields = fields
.parameters
.iter()
.filter(|parameter| parameter.is_keyword_group());

let group_keyword_param_exists = keyword_group_fields.peekable().peek().is_some();

let match_keyword_parameters_arms = fields
.parameters
.iter()
Expand All @@ -597,6 +643,55 @@ fn generate_impl_filter_parameters(filter_parameters: &FilterParameters<'_>) ->
quote!{ let #field = #field.ok_or_else(|| ::liquid_core::error::Error::with_msg(concat!("Expected named argument `", #liquid_name, "`")))?; }
});

let keyword_group_fields_handling_blocks = fields
.parameters
.iter()
.filter(|parameter| parameter.is_keyword_group())
.map(generate_construct_keyword_group_field)
.collect::<Vec<_>>();

let keyword_not_found_in_params_block = if group_keyword_param_exists {
// If there is a parameter that indicates all keywords should be grouped
// in an object, we generate an empty matching arm to prevent an error from
// being returned when a parsed keyword argument is not defines as a param.
quote! {
{}
}
} else {
// If there is no parameter that indicates all keywords should be grouped,
// an error is returned when a keyword argument is found but has not being
// declared.
quote! {
{
return ::std::result::Result::Err(::liquid_core::error::Error::with_msg(format!("Unexpected named argument `{}`", keyword)))
}
}
};

let assign_grouped_keyword_block = if group_keyword_param_exists {
keyword_group_fields_handling_blocks
.first()
.unwrap()
.clone()
} else {
quote! {}
};

let keywords_handling_block = quote! {
let mut keyword_as_map: std::collections::HashMap<String, liquid_core::runtime::Expression> = std::collections::HashMap::new();
#(let mut #keyword_fields = ::std::option::Option::None;)*
#[allow(clippy::never_loop)] // This is not obfuscating the code because it's generated by a macro
while let ::std::option::Option::Some(arg) = args.keyword.next() {
keyword_as_map.insert(arg.0.into(), arg.1.clone());
match arg.0 {
#(#match_keyword_parameters_arms)*
keyword => #keyword_not_found_in_params_block
}
}
#assign_grouped_keyword_block
#(#unwrap_required_keyword_fields)*
};

quote! {
impl<'a> ::liquid_core::parser::FilterParameters<'a> for #name {
type EvaluatedFilterParameters = #evaluated_name<'a>;
Expand All @@ -606,18 +701,10 @@ fn generate_impl_filter_parameters(filter_parameters: &FilterParameters<'_>) ->
if let ::std::option::Option::Some(arg) = args.positional.next() {
return ::std::result::Result::Err(#too_many_args);
}

#(let mut #keyword_fields = ::std::option::Option::None;)*
#[allow(clippy::never_loop)] // This is not obfuscating the code because it's generated by a macro
while let ::std::option::Option::Some(arg) = args.keyword.next() {
match arg.0 {
#(#match_keyword_parameters_arms)*
keyword => return ::std::result::Result::Err(::liquid_core::error::Error::with_msg(format!("Unexpected named argument `{}`", keyword))),
}
}
#(#unwrap_required_keyword_fields)*
#keywords_handling_block

Ok( #name { #comma_separated_field_names } )

}

fn evaluate(&'a self, runtime: &'a dyn ::liquid_core::runtime::Runtime) -> ::liquid_core::error::Result<Self::EvaluatedFilterParameters> {
Expand Down Expand Up @@ -692,6 +779,12 @@ fn generate_impl_reflection(filter_parameters: &FilterParameters<'_>) -> TokenSt
.filter(|parameter| parameter.is_keyword())
.map(generate_parameter_reflection);

let kwg_params_reflection = fields
.parameters
.iter()
.filter(|parameter| parameter.is_keyword_group())
.map(generate_parameter_reflection);

let pos_params_reflection = fields
.parameters
.iter()
Expand All @@ -707,6 +800,10 @@ fn generate_impl_reflection(filter_parameters: &FilterParameters<'_>) -> TokenSt
fn keyword_parameters() -> &'static [::liquid_core::parser::ParameterReflection] {
&[ #(#kw_params_reflection)* ]
}

fn keyword_group_parameters() -> &'static [::liquid_core::parser::ParameterReflection] {
&[ #(#kwg_params_reflection)* ]
}
}
}
}
Expand Down
11 changes: 9 additions & 2 deletions crates/derive/src/parse_filter/filter_reflection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ fn generate_reflection(filter_parser: &ParseFilter<'_>) -> Result<TokenStream> {
let impl_filter_reflection =
filter_parser.generate_impl(quote! { ::liquid_core::parser::FilterReflection });

let (positional_parameters, keyword_parameters) = if let Some(parameters_struct_name) =
let (positional_parameters, keyword_parameters, keyword_group_parameters) = if let Some(
parameters_struct_name,
) =
parameters_struct_name
{
(
quote_spanned! {parameters_struct_name.span()=> <#parameters_struct_name as ::liquid_core::parser::FilterParametersReflection>::positional_parameters() },
quote_spanned! {parameters_struct_name.span()=> <#parameters_struct_name as ::liquid_core::parser::FilterParametersReflection>::keyword_parameters() },
quote_spanned! {parameters_struct_name.span()=> <#parameters_struct_name as ::liquid_core::parser::FilterParametersReflection>::keyword_group_parameters() },
)
} else {
(quote! { &[] }, quote! { &[] })
(quote! { &[] }, quote! { &[] }, quote! { &[] })
};

Ok(quote! {
Expand All @@ -42,6 +45,10 @@ fn generate_reflection(filter_parser: &ParseFilter<'_>) -> Result<TokenStream> {
fn keyword_parameters(&self) -> &'static [::liquid_core::parser::ParameterReflection] {
#keyword_parameters
}

fn keyword_group_parameters(&self) -> &'static [::liquid_core::parser::ParameterReflection] {
#keyword_group_parameters
}
}
})
}
Expand Down
Loading
Loading