@@ -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 allowed." ,
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> {
279307enum FilterParameterMode {
280308 Keyword ,
281309 Positional ,
310+ KeywordGroup ,
282311}
283312
284313impl 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`
428467fn 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}
0 commit comments