Skip to main content
This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew_macro/derive_props/
field.rs

1use std::cmp::{Ord, Ordering, PartialEq, PartialOrd};
2use std::convert::TryFrom;
3
4use proc_macro2::{Ident, Span};
5use quote::{format_ident, quote, quote_spanned};
6use syn::parse::Result;
7use syn::spanned::Spanned;
8use syn::{
9    parse_quote, Attribute, Error, Expr, Field, GenericArgument, GenericParam, Generics,
10    PathArguments, Type, Visibility,
11};
12
13use super::should_preserve_attr;
14use crate::derive_props::generics::push_type_param;
15
16fn is_option_type(ty: &Type) -> bool {
17    if let Type::Path(type_path) = ty {
18        if let Some(segment) = type_path.path.segments.last() {
19            if segment.ident == "Option" {
20                if let PathArguments::AngleBracketed(args) = &segment.arguments {
21                    return args.args.len() == 1
22                        && matches!(args.args.first(), Some(GenericArgument::Type(_)));
23                }
24            }
25        }
26    }
27    false
28}
29
30#[allow(clippy::large_enum_variant)]
31#[derive(PartialEq, Eq)]
32pub enum PropAttr {
33    Required { wrapped_name: Ident },
34    PropOr(Expr),
35    PropOrElse(Expr),
36    PropOrDefault,
37}
38
39#[derive(Eq)]
40pub struct PropField {
41    pub ty: Type,
42    name: Ident,
43    pub attr: PropAttr,
44    extra_attrs: Vec<Attribute>,
45}
46
47impl PropField {
48    /// All required property fields are wrapped in an `Option`
49    pub fn is_required(&self) -> bool {
50        matches!(self.attr, PropAttr::Required { .. })
51    }
52
53    /// This check name is descriptive to help a developer realize they missed a required prop
54    fn to_check_name(&self, props_name: &Ident) -> Ident {
55        format_ident!("Has{}{}", props_name, self.name, span = Span::mixed_site())
56    }
57
58    /// This check name is descriptive to help a developer realize they missed a required prop
59    fn to_check_arg_name(&self, props_name: &Ident) -> GenericParam {
60        let ident = format_ident!("How{}{}", props_name, self.name, span = Span::mixed_site());
61        GenericParam::Type(ident.into())
62    }
63
64    /// Ident of the wrapped field name
65    fn wrapped_name(&self) -> &Ident {
66        match &self.attr {
67            PropAttr::Required { wrapped_name } => wrapped_name,
68            _ => &self.name,
69        }
70    }
71
72    pub fn to_field_check<'a>(
73        &'a self,
74        props_name: &'a Ident,
75        vis: &'a Visibility,
76        token: &'a GenericParam,
77    ) -> PropFieldCheck<'a> {
78        let check_struct = self.to_check_name(props_name);
79        let check_arg = self.to_check_arg_name(props_name);
80        PropFieldCheck {
81            this: self,
82            vis,
83            token,
84            check_struct,
85            check_arg,
86        }
87    }
88
89    /// Used to transform the `PropWrapper` struct into `Properties`
90    pub fn to_field_setter(&self) -> proc_macro2::TokenStream {
91        let name = &self.name;
92        let setter = match &self.attr {
93            PropAttr::Required { wrapped_name } => {
94                quote! {
95                    #name: ::std::option::Option::unwrap(this.wrapped.#wrapped_name),
96                }
97            }
98            PropAttr::PropOr(value) => {
99                quote_spanned! {value.span()=>
100                    #name: ::std::option::Option::unwrap_or(this.wrapped.#name, #value),
101                }
102            }
103            PropAttr::PropOrElse(func) => {
104                quote_spanned! {func.span()=>
105                    #name: ::std::option::Option::unwrap_or_else(this.wrapped.#name, #func),
106                }
107            }
108            PropAttr::PropOrDefault => {
109                quote! {
110                    #name: ::std::option::Option::unwrap_or_default(this.wrapped.#name),
111                }
112            }
113        };
114        let extra_attrs = &self.extra_attrs;
115        quote! {
116            #( #extra_attrs )*
117            #setter
118        }
119    }
120
121    /// Wrap all required props in `Option`
122    pub fn to_field_def(&self) -> proc_macro2::TokenStream {
123        let ty = &self.ty;
124        let extra_attrs = &self.extra_attrs;
125        let wrapped_name = self.wrapped_name();
126        quote! {
127            #( #extra_attrs )*
128            #wrapped_name: ::std::option::Option<#ty>,
129        }
130    }
131
132    /// All optional props must implement the `Default` trait
133    pub fn to_default_setter(&self) -> proc_macro2::TokenStream {
134        let wrapped_name = self.wrapped_name();
135        let extra_attrs = &self.extra_attrs;
136        quote! {
137            #( #extra_attrs )*
138            #wrapped_name: ::std::option::Option::None,
139        }
140    }
141
142    /// Each field is set using a builder method
143    pub fn to_build_step_fn(
144        &self,
145        vis: &Visibility,
146        props_name: &Ident,
147    ) -> proc_macro2::TokenStream {
148        let Self { name, ty, attr, .. } = self;
149        let token_ty = Ident::new("__YewTokenTy", Span::mixed_site());
150        let none_fn_name = format_ident!("{}_none", name, span = Span::mixed_site());
151        let build_fn = match attr {
152            PropAttr::Required { wrapped_name } => {
153                let check_struct = self.to_check_name(props_name);
154                let none_setter = if is_option_type(ty) {
155                    quote! {
156                        #[doc(hidden)]
157                        #vis fn #none_fn_name<#token_ty>(
158                            &mut self,
159                            token: #token_ty,
160                        ) -> #check_struct< #token_ty > {
161                            self.wrapped.#wrapped_name = ::std::option::Option::Some(::std::option::Option::None);
162                            #check_struct ( ::std::marker::PhantomData )
163                        }
164                    }
165                } else {
166                    quote! {}
167                };
168                quote! {
169                    #[doc(hidden)]
170                    #vis fn #name<#token_ty>(
171                        &mut self,
172                        token: #token_ty,
173                        value: impl ::yew::html::IntoPropValue<#ty>,
174                    ) -> #check_struct< #token_ty > {
175                        self.wrapped.#wrapped_name = ::std::option::Option::Some(value.into_prop_value());
176                        #check_struct ( ::std::marker::PhantomData )
177                    }
178
179                    #none_setter
180                }
181            }
182            _ => {
183                let none_setter = if is_option_type(ty) {
184                    quote! {
185                        #[doc(hidden)]
186                        #vis fn #none_fn_name<#token_ty>(
187                            &mut self,
188                            token: #token_ty,
189                        ) -> #token_ty {
190                            self.wrapped.#name = ::std::option::Option::Some(::std::option::Option::None);
191                            token
192                        }
193                    }
194                } else {
195                    quote! {}
196                };
197                quote! {
198                    #[doc(hidden)]
199                    #vis fn #name<#token_ty>(
200                        &mut self,
201                        token: #token_ty,
202                        value: impl ::yew::html::IntoPropValue<#ty>,
203                    ) -> #token_ty {
204                        self.wrapped.#name = ::std::option::Option::Some(value.into_prop_value());
205                        token
206                    }
207
208                    #none_setter
209                }
210            }
211        };
212        let extra_attrs = &self.extra_attrs;
213        quote! {
214            #( #extra_attrs )*
215            #build_fn
216        }
217    }
218
219    // Detect Properties 2.0 attributes
220    fn attribute(named_field: &Field) -> Result<PropAttr> {
221        let attr = named_field.attrs.iter().find(|attr| {
222            attr.path().is_ident("prop_or")
223                || attr.path().is_ident("prop_or_else")
224                || attr.path().is_ident("prop_or_default")
225        });
226
227        if let Some(attr) = attr {
228            if attr.path().is_ident("prop_or") {
229                Ok(PropAttr::PropOr(attr.parse_args()?))
230            } else if attr.path().is_ident("prop_or_else") {
231                Ok(PropAttr::PropOrElse(attr.parse_args()?))
232            } else if attr.path().is_ident("prop_or_default") {
233                Ok(PropAttr::PropOrDefault)
234            } else {
235                unreachable!()
236            }
237        } else {
238            let ident = named_field.ident.as_ref().unwrap();
239            let wrapped_name = format_ident!("{}_wrapper", ident, span = Span::mixed_site());
240            Ok(PropAttr::Required { wrapped_name })
241        }
242    }
243}
244
245pub struct PropFieldCheck<'a> {
246    this: &'a PropField,
247    vis: &'a Visibility,
248    token: &'a GenericParam,
249    check_struct: Ident,
250    check_arg: GenericParam,
251}
252
253impl PropFieldCheck<'_> {
254    pub fn to_fake_prop_decl(&self) -> proc_macro2::TokenStream {
255        let Self { this, .. } = self;
256        if !this.is_required() {
257            return Default::default();
258        }
259        let mut prop_check_name = this.name.clone();
260        prop_check_name.set_span(Span::mixed_site());
261        quote! {
262            #[allow(non_camel_case_types)]
263            pub struct #prop_check_name;
264        }
265    }
266
267    pub fn to_stream(
268        &self,
269        type_generics: &mut Generics,
270        check_args: &mut Vec<GenericParam>,
271        prop_name_mod: &Ident,
272    ) -> proc_macro2::TokenStream {
273        let Self {
274            this,
275            vis,
276            token,
277            check_struct,
278            check_arg,
279        } = self;
280        if !this.is_required() {
281            return Default::default();
282        }
283        let mut prop_check_name = this.name.clone();
284        prop_check_name.set_span(Span::mixed_site());
285        check_args.push(check_arg.clone());
286        push_type_param(type_generics, check_arg.clone());
287        let where_clause = type_generics.make_where_clause();
288        where_clause.predicates.push(parse_quote! {
289            #token: ::yew::html::HasProp< #prop_name_mod :: #prop_check_name, #check_arg >
290        });
291
292        quote! {
293            #[doc(hidden)]
294            #[allow(non_camel_case_types)]
295            #vis struct #check_struct<How>(::std::marker::PhantomData<How>);
296
297            #[automatically_derived]
298            #[diagnostic::do_not_recommend]
299            impl<B> ::yew::html::HasProp< #prop_name_mod :: #prop_check_name, #check_struct<B>>
300                for #check_struct<B> {}
301
302            #[automatically_derived]
303            #[diagnostic::do_not_recommend]
304            impl<B, P, How> ::yew::html::HasProp<P, &dyn ::yew::html::HasProp<P, How>>
305                for #check_struct<B>
306                where B: ::yew::html::HasProp<P, How> {}
307
308        }
309    }
310}
311
312impl TryFrom<Field> for PropField {
313    type Error = Error;
314
315    fn try_from(field: Field) -> Result<Self> {
316        let extra_attrs = field
317            .attrs
318            .iter()
319            .filter(|a| should_preserve_attr(a))
320            .cloned()
321            .collect();
322
323        Ok(PropField {
324            attr: Self::attribute(&field)?,
325            extra_attrs,
326            ty: field.ty,
327            name: field.ident.unwrap(),
328        })
329    }
330}
331
332impl PartialOrd for PropField {
333    fn partial_cmp(&self, other: &PropField) -> Option<Ordering> {
334        Some(self.cmp(other))
335    }
336}
337
338impl Ord for PropField {
339    fn cmp(&self, other: &PropField) -> Ordering {
340        if self.name == other.name {
341            Ordering::Equal
342        } else if self.name == "children" {
343            Ordering::Greater
344        } else if other.name == "children" {
345            Ordering::Less
346        } else {
347            self.name.cmp(&other.name)
348        }
349    }
350}
351
352impl PartialEq for PropField {
353    fn eq(&self, other: &Self) -> bool {
354        self.name == other.name
355    }
356}