Skip to content

Commit

Permalink
feat: Support arbitrary keyword args on filtes
Browse files Browse the repository at this point in the history
  • Loading branch information
juanazam committed Sep 18, 2023
1 parent 378fd03 commit e597967
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 12 deletions.
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

0 comments on commit e597967

Please sign in to comment.