Skip to content

Commit 51c048c

Browse files
committed
feat: support arbitrary keyword args on filtes
1 parent 378fd03 commit 51c048c

File tree

7 files changed

+287
-12
lines changed

7 files changed

+287
-12
lines changed

crates/core/src/parser/filter.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub struct ParameterReflection {
2424
pub trait FilterParametersReflection {
2525
fn positional_parameters() -> &'static [ParameterReflection];
2626
fn keyword_parameters() -> &'static [ParameterReflection];
27+
fn keyword_group_parameters() -> &'static [ParameterReflection];
2728
}
2829

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

4344
fn positional_parameters(&self) -> &'static [ParameterReflection];
4445
fn keyword_parameters(&self) -> &'static [ParameterReflection];
46+
fn keyword_group_parameters(&self) -> &'static [ParameterReflection];
4547
}
4648

4749
/// A trait that declares and holds the parameters of a filter.

crates/core/src/runtime/expression.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::fmt;
22

33
use crate::error::Result;
4+
use crate::model::Object;
45
use crate::model::Scalar;
56
use crate::model::Value;
67
use crate::model::ValueCow;
@@ -9,26 +10,39 @@ use crate::model::ValueView;
910
use super::variable::Variable;
1011
use super::Runtime;
1112

13+
use std::collections::HashMap;
14+
1215
/// An un-evaluated `Value`.
1316
#[derive(Debug, Clone, PartialEq)]
1417
pub enum Expression {
1518
/// Un-evaluated.
1619
Variable(Variable),
1720
/// Evaluated.
1821
Literal(Value),
22+
/// Used for evaluating object literals,
23+
ObjectLiteral(ObjectLiteral),
1924
}
2025

26+
type ObjectLiteral = HashMap<String, Expression>;
27+
2128
impl Expression {
2229
/// Create an expression from a scalar literal.
2330
pub fn with_literal<S: Into<Scalar>>(literal: S) -> Self {
2431
Expression::Literal(Value::scalar(literal))
2532
}
2633

34+
/// Creates an expression from an object literal (used when parsing filter
35+
/// arguments)
36+
pub fn with_object_literal(object_literal_expr: ObjectLiteral) -> Self {
37+
Expression::ObjectLiteral(object_literal_expr)
38+
}
39+
2740
/// Convert into a literal if possible.
2841
pub fn into_literal(self) -> Option<Value> {
2942
match self {
3043
Expression::Literal(x) => Some(x),
3144
Expression::Variable(_) => None,
45+
Expression::ObjectLiteral(_) => None,
3246
}
3347
}
3448

@@ -37,6 +51,7 @@ impl Expression {
3751
match self {
3852
Expression::Literal(_) => None,
3953
Expression::Variable(x) => Some(x),
54+
Expression::ObjectLiteral(_) => None,
4055
}
4156
}
4257

@@ -48,6 +63,16 @@ impl Expression {
4863
let path = x.try_evaluate(runtime)?;
4964
runtime.try_get(&path)
5065
}
66+
Expression::ObjectLiteral(ref obj_lit) => {
67+
let obj = obj_lit
68+
.iter()
69+
.map(|(key, expr)| match expr.try_evaluate(runtime) {
70+
Some(result) => (key.into(), result.to_value()),
71+
None => (key.into(), Value::Nil),
72+
})
73+
.collect::<Object>();
74+
Some(ValueCow::Owned(obj.to_value()))
75+
}
5176
}
5277
}
5378

@@ -57,8 +82,14 @@ impl Expression {
5782
Expression::Literal(ref x) => ValueCow::Borrowed(x),
5883
Expression::Variable(ref x) => {
5984
let path = x.evaluate(runtime)?;
85+
6086
runtime.get(&path)?
6187
}
88+
Expression::ObjectLiteral(obj_lit) => obj_lit
89+
.iter()
90+
.map(|(key, expr)| (key.into(), expr.evaluate(runtime).unwrap().to_value()))
91+
.collect::<Object>()
92+
.into(),
6293
};
6394
Ok(val)
6495
}
@@ -69,6 +100,7 @@ impl fmt::Display for Expression {
69100
match self {
70101
Expression::Literal(ref x) => write!(f, "{}", x.source()),
71102
Expression::Variable(ref x) => write!(f, "{}", x),
103+
Expression::ObjectLiteral(ref x) => write!(f, "{:?}", x),
72104
}
73105
}
74106
}

crates/derive/src/filter_parameters.rs

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@ impl<'a> FilterParameters<'a> {
8383
));
8484
}
8585

86+
if fields.more_than_one_keyword_group_parameter() {
87+
let grouped_keyword_fields = fields
88+
.parameters
89+
.iter()
90+
.filter(|parameter| parameter.is_keyword_group())
91+
.collect::<Vec<_>>();
92+
93+
return Err(Error::new_spanned(
94+
grouped_keyword_fields.first(),
95+
"Found more than one keyword_group parameter, this is not allowd.",
96+
));
97+
}
98+
8699
let name = ident;
87100
let evaluated_name = Self::parse_attrs(attrs)?
88101
.unwrap_or_else(|| Ident::new(&format!("Evaluated{}", name), Span::call_site()));
@@ -115,6 +128,16 @@ impl<'a> FilterParametersFields<'a> {
115128
.find(|parameter| !parameter.is_optional())
116129
}
117130

131+
/// Predicate that indicates the presence of more than one keyword group
132+
/// argument
133+
fn more_than_one_keyword_group_parameter(&self) -> bool {
134+
self.parameters
135+
.iter()
136+
.filter(|parameter| parameter.is_keyword_group())
137+
.count()
138+
> 1
139+
}
140+
118141
/// Tries to create a new `FilterParametersFields` from the given `Fields`
119142
fn from_fields(fields: &'a Fields) -> Result<Self> {
120143
match fields {
@@ -256,6 +279,11 @@ impl<'a> FilterParameter<'a> {
256279
self.meta.mode == FilterParameterMode::Keyword
257280
}
258281

282+
/// Returns whether this is a keyword list field.
283+
fn is_keyword_group(&self) -> bool {
284+
self.meta.mode == FilterParameterMode::KeywordGroup
285+
}
286+
259287
/// Returns the name of this parameter in liquid.
260288
///
261289
/// That is, by default, the name of the field as a string. However,
@@ -279,13 +307,15 @@ impl<'a> ToTokens for FilterParameter<'a> {
279307
enum FilterParameterMode {
280308
Keyword,
281309
Positional,
310+
KeywordGroup,
282311
}
283312

284313
impl FromStr for FilterParameterMode {
285314
type Err = String;
286315
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
287316
match s {
288317
"keyword" => Ok(FilterParameterMode::Keyword),
318+
"keyword_group" => Ok(FilterParameterMode::KeywordGroup),
289319
"positional" => Ok(FilterParameterMode::Positional),
290320
s => Err(format!(
291321
"Expected either \"keyword\" or \"positional\". Found \"{}\".",
@@ -424,6 +454,15 @@ fn generate_construct_positional_field(
424454
}
425455
}
426456

457+
/// Generates the statement that assigns the keyword list argument.
458+
fn generate_construct_keyword_group_field(field: &FilterParameter<'_>) -> TokenStream {
459+
let name = &field.name;
460+
461+
quote! {
462+
let #name = Expression::with_object_literal(keyword_as_map);
463+
}
464+
}
465+
427466
/// Generates the statement that evaluates the `Expression`
428467
fn generate_evaluate_field(field: &FilterParameter<'_>) -> TokenStream {
429468
let name = &field.name;
@@ -582,6 +621,13 @@ fn generate_impl_filter_parameters(filter_parameters: &FilterParameters<'_>) ->
582621
.iter()
583622
.filter(|parameter| parameter.is_keyword());
584623

624+
let keyword_group_fields = fields
625+
.parameters
626+
.iter()
627+
.filter(|parameter| parameter.is_keyword_group());
628+
629+
let group_keyword_param_exists = keyword_group_fields.peekable().peek().is_some();
630+
585631
let match_keyword_parameters_arms = fields
586632
.parameters
587633
.iter()
@@ -597,6 +643,55 @@ fn generate_impl_filter_parameters(filter_parameters: &FilterParameters<'_>) ->
597643
quote!{ let #field = #field.ok_or_else(|| ::liquid_core::error::Error::with_msg(concat!("Expected named argument `", #liquid_name, "`")))?; }
598644
});
599645

646+
let keyword_group_fields_handling_blocks = fields
647+
.parameters
648+
.iter()
649+
.filter(|parameter| parameter.is_keyword_group())
650+
.map(generate_construct_keyword_group_field)
651+
.collect::<Vec<_>>();
652+
653+
let keyword_not_found_in_params_block = if group_keyword_param_exists {
654+
// If there is a parameter that indicates all keywords should be grouped
655+
// in an object, we generate an empty matching arm to prevent an error from
656+
// being returned when a parsed keyword argument is not defines as a param.
657+
quote! {
658+
{}
659+
}
660+
} else {
661+
// If there is no parameter that indicates all keywords should be grouped,
662+
// an error is returned when a keyword argument is found but has not being
663+
// declared.
664+
quote! {
665+
{
666+
return ::std::result::Result::Err(::liquid_core::error::Error::with_msg(format!("Unexpected named argument `{}`", keyword)))
667+
}
668+
}
669+
};
670+
671+
let assign_grouped_keyword_block = if group_keyword_param_exists {
672+
keyword_group_fields_handling_blocks
673+
.first()
674+
.unwrap()
675+
.clone()
676+
} else {
677+
quote! {}
678+
};
679+
680+
let keywords_handling_block = quote! {
681+
let mut keyword_as_map: std::collections::HashMap<String, liquid_core::runtime::Expression> = std::collections::HashMap::new();
682+
#(let mut #keyword_fields = ::std::option::Option::None;)*
683+
#[allow(clippy::never_loop)] // This is not obfuscating the code because it's generated by a macro
684+
while let ::std::option::Option::Some(arg) = args.keyword.next() {
685+
keyword_as_map.insert(arg.0.into(), arg.1.clone());
686+
match arg.0 {
687+
#(#match_keyword_parameters_arms)*
688+
keyword => #keyword_not_found_in_params_block
689+
}
690+
}
691+
#assign_grouped_keyword_block
692+
#(#unwrap_required_keyword_fields)*
693+
};
694+
600695
quote! {
601696
impl<'a> ::liquid_core::parser::FilterParameters<'a> for #name {
602697
type EvaluatedFilterParameters = #evaluated_name<'a>;
@@ -606,18 +701,10 @@ fn generate_impl_filter_parameters(filter_parameters: &FilterParameters<'_>) ->
606701
if let ::std::option::Option::Some(arg) = args.positional.next() {
607702
return ::std::result::Result::Err(#too_many_args);
608703
}
609-
610-
#(let mut #keyword_fields = ::std::option::Option::None;)*
611-
#[allow(clippy::never_loop)] // This is not obfuscating the code because it's generated by a macro
612-
while let ::std::option::Option::Some(arg) = args.keyword.next() {
613-
match arg.0 {
614-
#(#match_keyword_parameters_arms)*
615-
keyword => return ::std::result::Result::Err(::liquid_core::error::Error::with_msg(format!("Unexpected named argument `{}`", keyword))),
616-
}
617-
}
618-
#(#unwrap_required_keyword_fields)*
704+
#keywords_handling_block
619705

620706
Ok( #name { #comma_separated_field_names } )
707+
621708
}
622709

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

782+
let kwg_params_reflection = fields
783+
.parameters
784+
.iter()
785+
.filter(|parameter| parameter.is_keyword_group())
786+
.map(generate_parameter_reflection);
787+
695788
let pos_params_reflection = fields
696789
.parameters
697790
.iter()
@@ -707,6 +800,10 @@ fn generate_impl_reflection(filter_parameters: &FilterParameters<'_>) -> TokenSt
707800
fn keyword_parameters() -> &'static [::liquid_core::parser::ParameterReflection] {
708801
&[ #(#kw_params_reflection)* ]
709802
}
803+
804+
fn keyword_group_parameters() -> &'static [::liquid_core::parser::ParameterReflection] {
805+
&[ #(#kwg_params_reflection)* ]
806+
}
710807
}
711808
}
712809
}

crates/derive/src/parse_filter/filter_reflection.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@ fn generate_reflection(filter_parser: &ParseFilter<'_>) -> Result<TokenStream> {
1515
let impl_filter_reflection =
1616
filter_parser.generate_impl(quote! { ::liquid_core::parser::FilterReflection });
1717

18-
let (positional_parameters, keyword_parameters) = if let Some(parameters_struct_name) =
18+
let (positional_parameters, keyword_parameters, keyword_group_parameters) = if let Some(
19+
parameters_struct_name,
20+
) =
1921
parameters_struct_name
2022
{
2123
(
2224
quote_spanned! {parameters_struct_name.span()=> <#parameters_struct_name as ::liquid_core::parser::FilterParametersReflection>::positional_parameters() },
2325
quote_spanned! {parameters_struct_name.span()=> <#parameters_struct_name as ::liquid_core::parser::FilterParametersReflection>::keyword_parameters() },
26+
quote_spanned! {parameters_struct_name.span()=> <#parameters_struct_name as ::liquid_core::parser::FilterParametersReflection>::keyword_group_parameters() },
2427
)
2528
} else {
26-
(quote! { &[] }, quote! { &[] })
29+
(quote! { &[] }, quote! { &[] }, quote! { &[] })
2730
};
2831

2932
Ok(quote! {
@@ -42,6 +45,10 @@ fn generate_reflection(filter_parser: &ParseFilter<'_>) -> Result<TokenStream> {
4245
fn keyword_parameters(&self) -> &'static [::liquid_core::parser::ParameterReflection] {
4346
#keyword_parameters
4447
}
48+
49+
fn keyword_group_parameters(&self) -> &'static [::liquid_core::parser::ParameterReflection] {
50+
#keyword_group_parameters
51+
}
4552
}
4653
})
4754
}

0 commit comments

Comments
 (0)