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