liquid_derive/
filter_parameters.rs

1use crate::helpers::{assign_str_value, parse_str_value, AssignOnce};
2use proc_macro2::{Ident, Span, TokenStream};
3use quote::{quote, ToTokens};
4use std::str::FromStr;
5use syn::punctuated::Punctuated;
6use syn::spanned::Spanned;
7use syn::{
8    Attribute, Data, DeriveInput, Error, Field, Fields, GenericArgument, Path, PathArguments,
9    PathSegment, Result, Token, Type, Visibility,
10};
11
12/// Struct that contains information to generate the necessary code for `FilterParameters`.
13struct FilterParameters<'a> {
14    name: &'a Ident,
15    evaluated_name: Ident,
16    fields: FilterParametersFields<'a>,
17    vis: &'a Visibility,
18}
19
20impl<'a> FilterParameters<'a> {
21    /// Searches for `#[evaluated(...)]` in order to parse `evaluated_name`.
22    fn parse_attrs(attrs: &[Attribute]) -> Result<Option<Ident>> {
23        let mut evaluated_attrs = attrs
24            .iter()
25            .filter(|attr| attr.path().is_ident("evaluated"));
26
27        match (evaluated_attrs.next(), evaluated_attrs.next()) {
28            (Some(attr), None) => Ok(Self::parse_evaluated_attr(attr)?.get_ident().cloned()),
29
30            (_, Some(attr)) => Err(Error::new_spanned(
31                attr,
32                "Found multiple definitions for `evaluated` attribute.",
33            )),
34
35            _ => Ok(None),
36        }
37    }
38
39    /// Parses `#[evaluated(...)]` attribute.
40    fn parse_evaluated_attr(attr: &Attribute) -> Result<Path> {
41        let mut ident = None;
42        attr.parse_nested_meta(|meta| {
43            ident = Some(meta.path);
44            Ok(())
45        })?;
46        ident.ok_or_else(|| Error::new(attr.span(), "expected ident"))
47    }
48
49    /// Tries to create a new `FilterParameters` from the given `DeriveInput`
50    fn from_input(input: &'a DeriveInput) -> Result<Self> {
51        let DeriveInput {
52            attrs,
53            vis,
54            generics,
55            data,
56            ident,
57        } = input;
58
59        if !generics.params.is_empty() {
60            return Err(Error::new_spanned(
61                generics,
62                "Generics are cannot be used in FilterParameters.",
63            ));
64        }
65
66        let fields = match data {
67            Data::Struct(data) => FilterParametersFields::from_fields(&data.fields)?,
68            Data::Enum(data) => {
69                return Err(Error::new_spanned(
70                    data.enum_token,
71                    "Enums cannot be FilterParameters.",
72                ));
73            }
74            Data::Union(data) => {
75                return Err(Error::new_spanned(
76                    data.union_token,
77                    "Unions cannot be FilterParameters.",
78                ));
79            }
80        };
81
82        if let Some(parameter) = fields.required_after_optional() {
83            return Err(Error::new_spanned(
84                parameter,
85                "Found required positional parameter after an optional positional parameter. The user can't input this parameters without inputting the optional ones first.",
86            ));
87        }
88
89        let name = ident;
90        let evaluated_name = Self::parse_attrs(attrs)?
91            .unwrap_or_else(|| Ident::new(&format!("Evaluated{name}"), Span::call_site()));
92
93        Ok(FilterParameters {
94            name,
95            evaluated_name,
96            fields,
97            vis,
98        })
99    }
100}
101
102/// Struct that contains `FilterParameter`s.
103struct FilterParametersFields<'a> {
104    parameters: Punctuated<FilterParameter<'a>, Token![,]>,
105}
106
107impl<'a> FilterParametersFields<'a> {
108    /// Returns the first required positional parameter (if any) that appears after an optional
109    /// positional parameter.
110    ///
111    /// All optional positional parameters must appear after every required positional parameter.
112    /// If this function returns `Some`, the macro is supposed to fail to compile.
113    fn required_after_optional(&self) -> Option<&FilterParameter<'_>> {
114        self.parameters
115            .iter()
116            .filter(|parameter| parameter.is_positional())
117            .skip_while(|parameter| parameter.is_required())
118            .find(|parameter| !parameter.is_optional())
119    }
120
121    /// Tries to create a new `FilterParametersFields` from the given `Fields`
122    fn from_fields(fields: &'a Fields) -> Result<Self> {
123        match fields {
124            Fields::Named(fields) => {
125                let parameters = fields
126                    .named
127                    .iter()
128                    .map(|field| {
129                        let name = field.ident.as_ref().expect("Fields are named.");
130                        FilterParameter::new(name, field)
131                    })
132                    .collect::<Result<Punctuated<_, Token![,]>>>()?;
133
134                if parameters.is_empty() {
135                    Err(Error::new_spanned(
136                        fields,
137                        "FilterParameters fields must have at least one field. To define an argumentless filter, just skip the `parameters(...)` element in `ParseFilter`.",
138                    ))
139                } else {
140                    Ok(Self { parameters })
141                }
142            }
143
144            Fields::Unnamed(fields) => {
145                Err(Error::new_spanned(
146                    fields,
147                    "FilterParameters fields must have explicit names. Tuple structs are not allowed.",
148                ))
149            }
150
151            Fields::Unit => {
152                Err(Error::new_spanned(
153                    fields,
154                    "FilterParameters fields must have at least one field. To define an argumentless filter, just skip the `parameters(...)` element in `ParseFilter`.",
155                ))
156            }
157        }
158    }
159}
160
161/// Information for a single parameter in a struct that implements `FilterParameters`.
162struct FilterParameter<'a> {
163    name: &'a Ident,
164    is_optional: bool,
165    meta: FilterParameterMeta,
166}
167
168impl<'a> FilterParameter<'a> {
169    /// This message is used a lot in other associated functions
170    const ERROR_INVALID_TYPE: &'static str = "Invalid type. All fields in FilterParameters must be either of type `Expression` or `Option<Expression>`";
171
172    /// Helper function for `validate_filter_parameter_fields()`.
173    /// Given `::liquid_core::runtime::Expression`, returns `Expression`.
174    fn get_type_name(ty: &Type) -> Result<&PathSegment> {
175        match ty {
176            Type::Path(ty) => {
177                let path = match ty.path.segments.last() {
178                    Some(path) => path,
179                    None => return Err(Error::new_spanned(ty, Self::ERROR_INVALID_TYPE)),
180                };
181
182                Ok(path)
183            }
184            ty => Err(Error::new_spanned(ty, Self::ERROR_INVALID_TYPE)),
185        }
186    }
187
188    /// Returns Some(true) if type is optional, Some(false) if it's not and Err if not a valid type.
189    ///
190    /// `Expression` => Some(false),
191    /// `Option<Expression>` => Some(true),
192    ///  _ => Err(...),
193    fn parse_type_is_optional(ty: &Type) -> Result<bool> {
194        let path = Self::get_type_name(ty)?;
195        match path.ident.to_string().as_str() {
196            "Option" => match &path.arguments {
197                PathArguments::AngleBracketed(arguments) => {
198                    let args = &arguments.args;
199                    if args.len() != 1 {
200                        return Err(Error::new_spanned(ty, Self::ERROR_INVALID_TYPE));
201                    }
202                    let arg = match args.last() {
203                        Some(arg) => arg,
204                        None => return Err(Error::new_spanned(ty, Self::ERROR_INVALID_TYPE)),
205                    };
206
207                    if let GenericArgument::Type(ty) = arg {
208                        let path = Self::get_type_name(ty)?;
209                        if path.ident.to_string().as_str() == "Expression"
210                            && path.arguments.is_empty()
211                        {
212                            return Ok(true);
213                        }
214                    }
215                    Err(Error::new_spanned(ty, Self::ERROR_INVALID_TYPE))
216                }
217                _ => Err(Error::new_spanned(ty, Self::ERROR_INVALID_TYPE)),
218            },
219            "Expression" => {
220                if !path.arguments.is_empty() {
221                    Err(Error::new_spanned(ty, Self::ERROR_INVALID_TYPE))
222                } else {
223                    Ok(false)
224                }
225            }
226            _ => Err(Error::new_spanned(ty, Self::ERROR_INVALID_TYPE)),
227        }
228    }
229
230    /// Creates a new `FilterParameter` from the given `field`, with the given `name`.
231    fn new(name: &'a Ident, field: &Field) -> Result<Self> {
232        let is_optional = Self::parse_type_is_optional(&field.ty)?;
233        let meta = FilterParameterMeta::from_field(field)?;
234
235        Ok(FilterParameter {
236            name,
237            is_optional,
238            meta,
239        })
240    }
241
242    /// Returns whether this field is optional.
243    fn is_optional(&self) -> bool {
244        self.is_optional
245    }
246
247    /// Returns whether this field is required (not optional).
248    fn is_required(&self) -> bool {
249        !self.is_optional
250    }
251
252    /// Returns whether this is a positional field.
253    fn is_positional(&self) -> bool {
254        self.meta.mode == FilterParameterMode::Positional
255    }
256
257    /// Returns whether this is a keyword field.
258    fn is_keyword(&self) -> bool {
259        self.meta.mode == FilterParameterMode::Keyword
260    }
261
262    /// Returns the name of this parameter in liquid.
263    ///
264    /// That is, by default, the name of the field as a string. However,
265    /// this name may be overridden by `rename` attribute.
266    fn liquid_name(&self) -> String {
267        match &self.meta.rename {
268            Some(name) => name.clone(),
269            None => self.name.to_string(),
270        }
271    }
272}
273
274impl ToTokens for FilterParameter<'_> {
275    fn to_tokens(&self, tokens: &mut TokenStream) {
276        self.name.to_tokens(tokens);
277    }
278}
279
280/// Whether `FilterParameter` is `Keyword` or `Positional`.
281#[derive(PartialEq)]
282enum FilterParameterMode {
283    Keyword,
284    Positional,
285}
286
287impl FromStr for FilterParameterMode {
288    type Err = String;
289    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
290        match s {
291            "keyword" => Ok(FilterParameterMode::Keyword),
292            "positional" => Ok(FilterParameterMode::Positional),
293            s => Err(format!(
294                "Expected either \"keyword\" or \"positional\". Found \"{s}\"."
295            )),
296        }
297    }
298}
299
300/// The type that this `FilterParameter` will evaluate to.
301enum FilterParameterType {
302    // Any value, the default type
303    Value,
304
305    // Scalars
306    Scalar,
307    Integer,
308    Float,
309    Bool,
310    DateTime,
311    Date,
312    Str,
313}
314
315impl FromStr for FilterParameterType {
316    type Err = String;
317    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
318        match s {
319            "any" => Ok(FilterParameterType::Value),
320            "scalar" => Ok(FilterParameterType::Scalar),
321            "integer" => Ok(FilterParameterType::Integer),
322            "float" => Ok(FilterParameterType::Float),
323            "bool" => Ok(FilterParameterType::Bool),
324            "date_time" => Ok(FilterParameterType::DateTime),
325            "date" => Ok(FilterParameterType::Date),
326            "str" => Ok(FilterParameterType::Str),
327            _ => Err(format!("Expected one of the following: \"any\", \"integer\", \"float\", \"bool\", \"date_time\", \"date\", \"scalar\", or \"str\". Found \"{s}\".")),
328        }
329    }
330}
331
332/// Struct that contains the information about `FilterParameter` parsed in `#[parameter(...)]` attribute.
333struct FilterParameterMeta {
334    rename: Option<String>,
335    description: String,
336    mode: FilterParameterMode,
337    ty: FilterParameterType,
338}
339
340impl FilterParameterMeta {
341    /// Tries to create a new `FilterParameterMeta` from the given `Attribute`
342    fn parse_parameter_attribute(attr: &Attribute) -> Result<Self> {
343        let mut rename = AssignOnce::Unset;
344        let mut description = AssignOnce::Unset;
345        let mut mode = AssignOnce::Unset;
346        let mut arg_type = AssignOnce::Unset;
347        attr.parse_nested_meta(|meta| {
348            if meta.path.is_ident("rename") {
349                assign_str_value(&mut rename, attr, "rename", &meta)?;
350            } else if meta.path.is_ident("description") {
351                assign_str_value(&mut description, attr, "description", &meta)?;
352            } else if meta.path.is_ident("mode") {
353                parse_str_value(&mut mode, attr, "mode", &meta)?;
354            } else if meta.path.is_ident("arg_type") {
355                parse_str_value(&mut arg_type, attr, "arg_type", &meta)?;
356            } else {
357                return Err(Error::new(
358                    attr.span(),
359                    format!(
360                        "unknown `{}` parameter attribute",
361                        meta.path.to_token_stream()
362                    ),
363                ));
364            }
365            Ok(())
366        })?;
367
368        let rename = rename.into_option();
369        let description = description.unwrap_or_err(|| Error::new_spanned(
370            attr,
371            "Found parameter without description. Description is necessary in order to properly generate ParameterReflection.",
372        ))?;
373        let mode = mode.default_to(FilterParameterMode::Positional);
374        let ty = arg_type.default_to(FilterParameterType::Value);
375
376        Ok(FilterParameterMeta {
377            rename,
378            description,
379            mode,
380            ty,
381        })
382    }
383
384    /// Tries to create a new `FilterParameterMeta` from the given field.
385    fn from_field(field: &Field) -> Result<Self> {
386        let mut parameter_attrs = field
387            .attrs
388            .iter()
389            .filter(|attr| attr.path().is_ident("parameter"));
390
391        match (parameter_attrs.next(), parameter_attrs.next()) {
392            (Some(attr), None) => Self::parse_parameter_attribute(attr),
393
394            (_, Some(attr)) => Err(Error::new_spanned(
395                attr,
396                "Found multiple definitions for `parameter` attribute.",
397            )),
398
399            _ => Err(Error::new_spanned(
400                field,
401                "Found parameter without #[parameter] attribute. All filter parameters must be accompanied by this attribute.",
402            )),
403        }
404    }
405}
406
407/// Generates the statement that assigns the next positional argument.
408fn generate_construct_positional_field(
409    field: &FilterParameter<'_>,
410    required: usize,
411) -> TokenStream {
412    let name = &field.name;
413
414    if field.is_optional() {
415        quote! {
416            let #name = args.positional.next();
417        }
418    } else {
419        let plural = if required == 1 { None } else { Some("s") };
420        quote! {
421            let #name = args.positional.next().ok_or_else(||
422                ::liquid_core::error::Error::with_msg("Invalid number of arguments")
423                    .context("cause", concat!("expected at least ", #required, " positional argument", #plural))
424            )?;
425        }
426    }
427}
428
429/// Generates the statement that evaluates the `Expression`
430fn generate_evaluate_field(field: &FilterParameter<'_>) -> TokenStream {
431    let name = &field.name;
432    let liquid_name = field.liquid_name();
433    let ty = &field.meta.ty;
434
435    let to_type = match ty {
436        FilterParameterType::Value => quote! { ::std::result::Result::Ok(#name) },
437        FilterParameterType::Scalar => quote! {
438            match #name {
439                ::liquid_core::ValueCow::Owned(v) => {
440                    v.as_scalar().into_owned()
441                },
442                ::liquid_core::ValueCow::Borrowed(v) => {
443                    v.as_scalar()
444                },
445            }.ok_or_else(||
446                ::liquid_core::error::Error::with_msg("Invalid argument")
447                    .context("argument", #liquid_name)
448                    .context("cause", "Scalar expected")
449            )
450        },
451        FilterParameterType::Integer => quote! {
452            #name.as_scalar()
453            .and_then(|s| s.to_integer())
454            .ok_or_else(||
455                ::liquid_core::error::Error::with_msg("Invalid argument")
456                    .context("argument", #liquid_name)
457                    .context("cause", "Whole number expected")
458            )
459        },
460        FilterParameterType::Float => quote! {
461            #name.as_scalar()
462            .and_then(|s| s.to_float())
463            .ok_or_else(||
464                ::liquid_core::error::Error::with_msg("Invalid argument")
465                    .context("argument", #liquid_name)
466                    .context("cause", "Fractional number expected")
467            )
468        },
469        FilterParameterType::Bool => quote! {
470            #name.as_scalar()
471            .and_then(|s| s.to_bool())
472            .ok_or_else(||
473                ::liquid_core::error::Error::with_msg("Invalid argument")
474                    .context("argument", #liquid_name)
475                    .context("cause", "Boolean expected")
476            )
477        },
478        FilterParameterType::DateTime => quote! {
479            #name.as_scalar()
480            .and_then(|s| s.to_date_time())
481            .ok_or_else(||
482                ::liquid_core::error::Error::with_msg("Invalid argument")
483                    .context("argument", #liquid_name)
484                    .context("cause", "DateTime expected")
485            )
486        },
487        FilterParameterType::Date => quote! {
488            #name.as_scalar()
489            .and_then(|s| s.to_date())
490            .ok_or_else(||
491                ::liquid_core::error::Error::with_msg("Invalid argument")
492                    .context("argument", #liquid_name)
493                    .context("cause", "Date expected")
494            )
495        },
496        FilterParameterType::Str => quote! {
497            match #name {
498                ::liquid_core::ValueCow::Owned(v) => {
499                    ::std::result::Result::Ok(v.to_kstr().into_owned().into())
500                },
501                ::liquid_core::ValueCow::Borrowed(v) => {
502                    ::std::result::Result::Ok(v.to_kstr())
503                },
504            }
505        },
506    };
507
508    if field.is_optional() {
509        quote! {
510            let #name = self.#name.as_ref().map(|field| {
511                let #name = field.evaluate(runtime)?;
512                let #name = #to_type?;
513                ::std::result::Result::Ok(#name)
514            }).transpose()?;
515        }
516    } else {
517        quote! {
518            let #name = self.#name.evaluate(runtime)?;
519            let #name = #to_type?;
520        }
521    }
522}
523
524/// Generates the match arm that assigns the given keyword argument.
525fn generate_keyword_match_arm(field: &FilterParameter<'_>) -> TokenStream {
526    let rust_name = &field.name;
527    let liquid_name = field.liquid_name();
528
529    quote! {
530        #liquid_name => if #rust_name.is_none() {
531            #rust_name = ::std::option::Option::Some(arg.1);
532        } else {
533            return ::std::result::Result::Err(::liquid_core::error::Error::with_msg(concat!("Multiple definitions of `", #liquid_name, "`")));
534        },
535    }
536}
537
538/// Generates implementation of `FilterParameters`.
539fn generate_impl_filter_parameters(filter_parameters: &FilterParameters<'_>) -> TokenStream {
540    let FilterParameters {
541        name,
542        evaluated_name,
543        fields,
544        ..
545    } = filter_parameters;
546
547    let num_min_positional = fields
548        .parameters
549        .iter()
550        .filter(|parameter| parameter.is_positional() && parameter.is_required())
551        .count();
552
553    let num_max_positional = fields
554        .parameters
555        .iter()
556        .filter(|parameter| parameter.is_positional())
557        .count();
558
559    let too_many_args = {
560        let plural = if num_max_positional == 1 {
561            None
562        } else {
563            Some("s")
564        };
565        quote! {
566            ::liquid_core::error::Error::with_msg("Invalid number of positional arguments")
567                .context("cause", concat!("expected at most ", #num_max_positional, " positional argument", #plural))
568        }
569    };
570
571    let field_names = fields.parameters.iter().map(|field| &field.name);
572    let comma_separated_field_names = quote! { #(#field_names,)* };
573
574    let evaluate_fields = fields.parameters.iter().map(generate_evaluate_field);
575
576    let construct_positional_fields = fields
577        .parameters
578        .iter()
579        .filter(|parameter| parameter.is_positional())
580        .map(|field| generate_construct_positional_field(field, num_min_positional));
581
582    let keyword_fields = fields
583        .parameters
584        .iter()
585        .filter(|parameter| parameter.is_keyword());
586
587    let match_keyword_parameters_arms = fields
588        .parameters
589        .iter()
590        .filter(|parameter| parameter.is_keyword())
591        .map(generate_keyword_match_arm);
592
593    let unwrap_required_keyword_fields = fields
594        .parameters
595        .iter()
596        .filter(|parameter| parameter.is_keyword() && parameter.is_required())
597        .map(|field| {
598            let liquid_name = field.liquid_name();
599            quote!{ let #field = #field.ok_or_else(|| ::liquid_core::error::Error::with_msg(concat!("Expected named argument `", #liquid_name, "`")))?; }
600        });
601
602    quote! {
603        impl<'a> ::liquid_core::parser::FilterParameters<'a> for #name {
604            type EvaluatedFilterParameters = #evaluated_name<'a>;
605
606            fn from_args(mut args: ::liquid_core::parser::FilterArguments) -> ::liquid_core::error::Result<Self> {
607                #![allow(clippy::ref_option_ref)]
608
609                #(#construct_positional_fields)*
610                if let ::std::option::Option::Some(arg) = args.positional.next() {
611                    return ::std::result::Result::Err(#too_many_args);
612                }
613
614                #(let mut #keyword_fields = ::std::option::Option::None;)*
615                #[allow(clippy::never_loop)] // This is not obfuscating the code because it's generated by a macro
616                while let ::std::option::Option::Some(arg) = args.keyword.next() {
617                    match arg.0 {
618                        #(#match_keyword_parameters_arms)*
619                        keyword => return ::std::result::Result::Err(::liquid_core::error::Error::with_msg(format!("Unexpected named argument `{}`", keyword))),
620                    }
621                }
622                #(#unwrap_required_keyword_fields)*
623
624                Ok( #name { #comma_separated_field_names } )
625            }
626
627            fn evaluate(&'a self, runtime: &'a dyn ::liquid_core::runtime::Runtime) -> ::liquid_core::error::Result<Self::EvaluatedFilterParameters> {
628                #![allow(clippy::ref_option_ref)]
629
630                #(#evaluate_fields)*
631
632                Ok( #evaluated_name { #comma_separated_field_names __phantom_data: ::std::marker::PhantomData } )
633            }
634        }
635    }
636}
637
638/// Generates `EvaluatedFilterParameters` struct.
639fn generate_evaluated_struct(filter_parameters: &FilterParameters<'_>) -> TokenStream {
640    let FilterParameters {
641        evaluated_name,
642        fields,
643        vis,
644        ..
645    } = filter_parameters;
646
647    let field_types = fields.parameters.iter().map(|field| {
648        let ty = match &field.meta.ty {
649            FilterParameterType::Value => quote! { ::liquid_core::model::ValueCow<'a> },
650            FilterParameterType::Scalar => quote! { ::liquid_core::model::ScalarCow<'a> },
651            FilterParameterType::Integer => quote! { i64 },
652            FilterParameterType::Float => quote! { f64 },
653            FilterParameterType::Bool => quote! { bool },
654            FilterParameterType::DateTime => quote! { ::liquid_core::model::DateTime },
655            FilterParameterType::Date => quote! { ::liquid_core::model::Date },
656            FilterParameterType::Str => quote! { ::liquid_core::model::KStringCow<'a> },
657        };
658
659        if field.is_optional() {
660            quote! { ::std::option::Option< #ty > }
661        } else {
662            quote! { #ty }
663        }
664    });
665
666    let field_names = fields.parameters.iter().map(|field| &field.name);
667
668    quote! {
669        #[allow(clippy::ref_option_ref)]
670        #vis struct #evaluated_name <'a>{
671            #(#field_names : #field_types,)*
672            __phantom_data: ::std::marker::PhantomData<&'a ()>
673        }
674    }
675}
676
677/// Constructs `ParameterReflection` for the given parameter.
678fn generate_parameter_reflection(field: &FilterParameter<'_>) -> TokenStream {
679    let name = field.liquid_name();
680    let description = &field.meta.description;
681    let is_optional = field.is_optional();
682
683    quote! {
684        ::liquid_core::parser::ParameterReflection {
685            name: #name,
686            description: #description,
687            is_optional: #is_optional,
688        },
689    }
690}
691
692/// Generates implementation of `FilterParametersReflection`.
693fn generate_impl_reflection(filter_parameters: &FilterParameters<'_>) -> TokenStream {
694    let FilterParameters { name, fields, .. } = filter_parameters;
695
696    let kw_params_reflection = fields
697        .parameters
698        .iter()
699        .filter(|parameter| parameter.is_keyword())
700        .map(generate_parameter_reflection);
701
702    let pos_params_reflection = fields
703        .parameters
704        .iter()
705        .filter(|parameter| parameter.is_positional())
706        .map(generate_parameter_reflection);
707
708    quote! {
709        impl ::liquid_core::parser::FilterParametersReflection for #name {
710            fn positional_parameters() -> &'static [::liquid_core::parser::ParameterReflection] {
711                #![allow(clippy::ref_option_ref)]
712                &[ #(#pos_params_reflection)* ]
713            }
714
715            fn keyword_parameters() -> &'static [::liquid_core::parser::ParameterReflection] {
716                #![allow(clippy::ref_option_ref)]
717                &[ #(#kw_params_reflection)* ]
718            }
719        }
720    }
721}
722
723/// Helper function for `generate_impl_display`
724fn generate_access_positional_field_for_display(field: &FilterParameter<'_>) -> TokenStream {
725    let rust_name = &field.name;
726
727    if field.is_optional() {
728        quote! {
729            self.#rust_name.as_ref()
730        }
731    } else {
732        quote! {
733            ::std::option::Option::Some(&self.#rust_name)
734        }
735    }
736}
737
738/// Helper function for `generate_impl_display`
739fn generate_access_keyword_field_for_display(field: &FilterParameter<'_>) -> TokenStream {
740    let rust_name = &field.name;
741    let liquid_name = field.liquid_name();
742
743    if field.is_optional() {
744        quote! {
745            (#liquid_name, self.#rust_name.as_ref())
746        }
747    } else {
748        quote! {
749            (#liquid_name, ::std::option::Option::Some(&self.#rust_name))
750        }
751    }
752}
753
754/// Generates implementation of `Display`.
755fn generate_impl_display(filter_parameters: &FilterParameters<'_>) -> TokenStream {
756    let FilterParameters { name, fields, .. } = filter_parameters;
757
758    let positional_fields = fields
759        .parameters
760        .iter()
761        .filter(|parameter| parameter.is_positional())
762        .map(generate_access_positional_field_for_display);
763
764    let keyword_fields = fields
765        .parameters
766        .iter()
767        .filter(|parameter| parameter.is_keyword())
768        .map(generate_access_keyword_field_for_display);
769
770    quote! {
771        impl ::std::fmt::Display for #name {
772            fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
773                #![allow(clippy::ref_option_ref)]
774                let positional = [#(#positional_fields ,)*];
775                let keyword = [#(#keyword_fields ,)*];
776
777                let positional = positional
778                    .iter()
779                    .filter_map(|p: &::std::option::Option<&::liquid_core::runtime::Expression>| p.as_ref())
780                    .map(|p| p.to_string());
781                let keyword = keyword.iter().filter_map(|p: &(&str, ::std::option::Option<&::liquid_core::runtime::Expression>)| match p.1 {
782                    ::std::option::Option::Some(p1) => ::std::option::Option::Some(format!("{}: {}", p.0, p1)),
783                    ::std::option::Option::None => ::std::option::Option::None,
784                });
785
786                let parameters = positional
787                    .chain(keyword)
788                    .collect::<::std::vec::Vec<::std::string::String>>()
789                    .join(", ");
790
791                write!(
792                    f,
793                    "{}",
794                    parameters
795                )
796            }
797        }
798    }
799}
800
801pub(crate) fn derive(input: &DeriveInput) -> TokenStream {
802    let filter_parameters = match FilterParameters::from_input(input) {
803        Ok(filter_parameters) => filter_parameters,
804        Err(err) => return err.to_compile_error(),
805    };
806
807    let mut output = TokenStream::new();
808    output.extend(generate_impl_filter_parameters(&filter_parameters));
809    output.extend(generate_impl_reflection(&filter_parameters));
810    output.extend(generate_impl_display(&filter_parameters));
811    output.extend(generate_evaluated_struct(&filter_parameters));
812
813    output
814}