diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d7d986474..702e38be51f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - #### ⚡️ Features + - The derive props macro now supports Properties with lifetimes [[@jstarry], [#580](https://github.com/yewstack/yew/pull/580)] + - #### 🛠 Fixes - #### 🚨 Breaking changes diff --git a/crates/macro/src/derive_props.rs b/crates/macro/src/derive_props.rs deleted file mode 100644 index f8e2775608b..00000000000 --- a/crates/macro/src/derive_props.rs +++ /dev/null @@ -1,342 +0,0 @@ -use proc_macro2::{Ident, Span}; -use quote::{quote, ToTokens}; -use std::convert::{TryFrom, TryInto}; -use std::iter; -use syn::parse::{Parse, ParseStream, Result}; -use syn::punctuated; -use syn::spanned::Spanned; -use syn::{ - DeriveInput, Error, Field, GenericParam, Generics, Meta, MetaList, NestedMeta, Type, TypeParam, - Visibility, -}; - -struct PropField { - ty: Type, - name: Ident, - wrapped_name: Option, -} - -impl TryFrom for PropField { - type Error = Error; - - fn try_from(field: Field) -> Result { - Ok(PropField { - wrapped_name: Self::required_wrapper(&field)?, - ty: field.ty, - name: field.ident.unwrap(), - }) - } -} - -pub struct DerivePropsInput { - vis: Visibility, - generics: Generics, - props_name: Ident, - prop_fields: Vec, -} - -impl Parse for DerivePropsInput { - fn parse(input: ParseStream) -> Result { - let input: DeriveInput = input.parse()?; - let named_fields = match input.data { - syn::Data::Struct(data) => match data.fields { - syn::Fields::Named(fields) => fields.named, - _ => unimplemented!("only structs are supported"), - }, - _ => unimplemented!("only structs are supported"), - }; - - let mut prop_fields: Vec = named_fields - .into_iter() - .map(|f| f.try_into()) - .collect::>>()?; - - // Alphabetize - prop_fields.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap()); - - Ok(Self { - vis: input.vis, - props_name: input.ident, - generics: input.generics, - prop_fields, - }) - } -} - -impl ToTokens for DerivePropsInput { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let Self { - vis, - generics, - props_name, - .. - } = self; - let generic_params = &generics.params; - let generic_where = &generics.where_clause; - let generic_types = self.generic_types(); - - let wrapped_name = Ident::new(&format!("Wrapped{}", props_name), Span::call_site()); - let wrapped_field_defs = self.wrapped_field_defs(); - let wrapped_default_setters = self.wrapped_default_setters(); - - let builder_name = Ident::new(&format!("{}Builder", props_name), Span::call_site()); - let builder_step = Ident::new(&format!("{}BuilderStep", props_name), Span::call_site()); - let builder_step_names = self.builder_step_names(); - let builder_start_step = &builder_step_names[0]; - let builder_build_step = &builder_step_names[builder_step_names.len() - 1]; - let builder_steps = &builder_step_names; - let builder_step_repeat = iter::repeat(&builder_step); - let impl_builder_for_steps = self.impl_builder_for_steps(&builder_name, &builder_steps); - let builder_set_fields = self.builder_set_fields(); - let vis_repeat = iter::repeat(&vis); - - let expanded = quote! { - struct #wrapped_name#generics { - #(#wrapped_field_defs)* - } - - impl#generics ::std::default::Default for #wrapped_name<#generic_types> #generic_where { - fn default() -> Self { - #wrapped_name::<#generic_types> { - #(#wrapped_default_setters)* - } - } - } - - #( - #[doc(hidden)] - #vis_repeat struct #builder_steps; - )* - - #[doc(hidden)] - #vis trait #builder_step {} - - #(impl #builder_step_repeat for #builder_steps {})* - - #[doc(hidden)] - #vis struct #builder_name #generic_where { - wrapped: ::std::boxed::Box<#wrapped_name<#generic_types>>, - _marker: ::std::marker::PhantomData, - } - - #(#impl_builder_for_steps)* - - impl #generics #builder_name<#builder_build_step, #generic_types> #generic_where { - #[doc(hidden)] - #vis fn build(self) -> #props_name<#generic_types> { - #props_name::<#generic_types> { - #(#builder_set_fields)* - } - } - } - - impl #generics ::yew::html::Properties for #props_name<#generic_types> #generic_where { - type Builder = #builder_name<#builder_start_step, #generic_types>; - - fn builder() -> Self::Builder { - #builder_name { - wrapped: ::std::boxed::Box::new(::std::default::Default::default()), - _marker: ::std::marker::PhantomData, - } - } - } - }; - - tokens.extend(proc_macro2::TokenStream::from(expanded)); - } -} - -impl PropField { - fn required_wrapper(named_field: &syn::Field) -> Result> { - let meta_list = if let Some(meta_list) = Self::find_props_meta_list(named_field) { - meta_list - } else { - return Ok(None); - }; - - let expected_required = syn::Error::new(meta_list.span(), "expected `props(required)`"); - let first_nested = if let Some(first_nested) = meta_list.nested.first() { - first_nested - } else { - return Err(expected_required); - }; - - let word_ident = match first_nested { - punctuated::Pair::End(NestedMeta::Meta(Meta::Word(ident))) => ident, - _ => return Err(expected_required), - }; - - if word_ident != "required" { - return Err(expected_required); - } - - if let Some(ident) = &named_field.ident { - Ok(Some(Ident::new( - &format!("{}_wrapper", ident), - Span::call_site(), - ))) - } else { - unreachable!() - } - } - - fn find_props_meta_list(field: &syn::Field) -> Option { - let meta_list = field - .attrs - .iter() - .find_map(|attr| match attr.parse_meta().ok()? { - Meta::List(meta_list) => Some(meta_list), - _ => None, - })?; - - if meta_list.ident == "props" { - Some(meta_list) - } else { - None - } - } -} - -impl DerivePropsInput { - fn generic_types(&self) -> proc_macro2::TokenStream { - let generic_types = self.generics.params.iter().map(|param| match param { - GenericParam::Type(TypeParam { ident, .. }) => ident, - _ => unimplemented!("only generic types are supported"), - }); - quote! {#(#generic_types),*} - } - - fn builder_step_names(&self) -> Vec { - let mut step_names: Vec = self - .prop_fields - .iter() - .filter(|prop_field| prop_field.wrapped_name.is_some()) - .map(|prop_field| { - Ident::new( - &format!("{}_{}_is_required", self.props_name, prop_field.name), - Span::call_site(), - ) - }) - .collect(); - - step_names.push(Ident::new( - &format!("{}BuildStep", self.props_name), - Span::call_site(), - )); - - step_names - } - - fn wrapped_field_defs(&self) -> impl Iterator { - self.prop_fields.iter().map(|pf| { - let PropField { name, ty, .. } = &pf; - if let Some(wrapped_name) = &pf.wrapped_name { - quote! { - #wrapped_name: ::std::option::Option<#ty>, - } - } else { - quote! { - #name: #ty, - } - } - }) - } - - fn wrapped_default_setters(&self) -> impl Iterator { - self.prop_fields.iter().map(|pf| { - if let Some(wrapped_name) = &pf.wrapped_name { - quote! { - #wrapped_name: ::std::default::Default::default(), - } - } else { - let name = &pf.name; - quote! { - #name: ::std::default::Default::default(), - } - } - }) - } - - fn builder_set_fields(&self) -> impl Iterator { - self.prop_fields.iter().map(|pf| { - let name = &pf.name; - if let Some(wrapped_name) = &pf.wrapped_name { - quote! { - #name: self.wrapped.#wrapped_name.unwrap(), - } - } else { - quote! { - #name: self.wrapped.#name, - } - } - }) - } - - fn impl_builder_for_steps( - &self, - builder_name: &Ident, - builder_step_names: &[Ident], - ) -> proc_macro2::TokenStream { - let Self { vis, generics, .. } = self; - let generic_types = self.generic_types(); - let generic_where = &generics.where_clause; - - let mut fields_index = 0; - let mut token_stream = proc_macro2::TokenStream::new(); - - for (step, step_name) in builder_step_names.iter().enumerate() { - let mut optional_fields = Vec::new(); - let mut required_field = None; - - if fields_index >= self.prop_fields.len() { - break; - } - - while let Some(pf) = self.prop_fields.get(fields_index) { - fields_index += 1; - if pf.wrapped_name.is_some() { - required_field = Some(pf); - break; - } else { - optional_fields.push((&pf.name, &pf.ty)); - } - } - - let optional_prop_fn = optional_fields.into_iter().map(|(prop_name, prop_type)| { - quote! { - #[doc(hidden)] - #vis fn #prop_name(mut self, #prop_name: #prop_type) -> #builder_name<#step_name, #generic_types> { - self.wrapped.#prop_name = #prop_name; - self - } - } - }); - - let required_prop_fn = required_field.iter().map(|p| { - let prop_name = &p.name; - let prop_type = &p.ty; - let wrapped_name = p.wrapped_name.as_ref().unwrap(); - let next_step_name = &builder_step_names[step + 1]; - - quote! { - #[doc(hidden)] - #vis fn #prop_name(mut self, #prop_name: #prop_type) -> #builder_name<#next_step_name, #generic_types> { - self.wrapped.#wrapped_name = ::std::option::Option::Some(#prop_name); - #builder_name { - wrapped: self.wrapped, - _marker: ::std::marker::PhantomData, - } - } - } - }); - - token_stream.extend(quote! { - impl #generics #builder_name<#step_name, #generic_types> #generic_where { - #(#optional_prop_fn)* - #(#required_prop_fn)* - } - }); - } - token_stream - } -} diff --git a/crates/macro/src/derive_props/builder.rs b/crates/macro/src/derive_props/builder.rs new file mode 100644 index 00000000000..e374e3a7158 --- /dev/null +++ b/crates/macro/src/derive_props/builder.rs @@ -0,0 +1,194 @@ +//! The `PropsBuilder` constructs props in alphabetical order and enforces that required props have +//! been set before allowing the build to complete. Each property has a corresponding method in the +//! builder. Required property builder methods advance the builder to the next step, optional +//! properties can be added or skipped with no effect on the build step. Once all of required +//! properties have been set, the builder moves to the final build step which implements the +//! `build()` method. + +use super::generics::{to_arguments, with_param_bounds, GenericArguments}; +use super::{DerivePropsInput, PropField}; +use proc_macro2::{Ident, Span}; +use quote::{quote, ToTokens}; +use std::iter; + +pub struct PropsBuilder<'a> { + builder_name: &'a Ident, + step_trait: &'a Ident, + step_names: Vec, + props: &'a DerivePropsInput, + wrapper_name: &'a Ident, +} + +impl ToTokens for PropsBuilder<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { + builder_name, + step_trait, + step_names, + props, + wrapper_name, + } = self; + + let DerivePropsInput { + vis, + generics, + props_name, + .. + } = props; + + let step_trait_repeat = iter::repeat(step_trait); + let vis_repeat = iter::repeat(&vis); + + let build_step = self.build_step(); + let impl_steps = self.impl_steps(); + let set_fields = self.set_fields(); + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let turbofish_generics = ty_generics.as_turbofish(); + let generic_args = to_arguments(&generics, build_step.clone()); + + // Each builder step implements the `BuilderStep` trait and `step_generics` is used to + // enforce that. + let step_generic_param = Ident::new("YEW_PROPS_BUILDER_STEP", Span::call_site()); + let step_generics = with_param_bounds( + &generics, + step_generic_param.clone(), + step_trait.clone().to_owned(), + ); + + let builder = quote! { + #( + #[doc(hidden)] + #vis_repeat struct #step_names; + )* + + #[doc(hidden)] + #vis trait #step_trait {} + + #(impl #step_trait_repeat for #step_names {})* + + #[doc(hidden)] + #vis struct #builder_name#step_generics { + wrapped: ::std::boxed::Box<#wrapper_name#ty_generics>, + _marker: ::std::marker::PhantomData<#step_generic_param>, + } + + #(#impl_steps)* + + impl#impl_generics #builder_name<#generic_args> #where_clause { + #[doc(hidden)] + #vis fn build(self) -> #props_name#ty_generics { + #props_name#turbofish_generics { + #(#set_fields)* + } + } + } + }; + + tokens.extend(builder); + } +} + +impl<'a> PropsBuilder<'_> { + pub fn new( + name: &'a Ident, + step_trait: &'a Ident, + props: &'a DerivePropsInput, + wrapper_name: &'a Ident, + ) -> PropsBuilder<'a> { + PropsBuilder { + builder_name: name, + step_trait, + step_names: Self::build_step_names(step_trait, &props.prop_fields), + props, + wrapper_name, + } + } +} + +impl PropsBuilder<'_> { + pub fn first_step_generic_args(&self) -> GenericArguments { + to_arguments(&self.props.generics, self.first_step().clone()) + } + + fn first_step(&self) -> &Ident { + &self.step_names[0] + } + + fn build_step(&self) -> &Ident { + &self.step_names[self.step_names.len() - 1] + } + + fn build_step_names(prefix: &Ident, prop_fields: &[PropField]) -> Vec { + let mut step_names: Vec = prop_fields + .iter() + .filter(|pf| pf.is_required()) + .map(|pf| pf.to_step_name(prefix)) + .collect(); + step_names.push(Ident::new(&format!("{}_build", prefix), Span::call_site())); + step_names + } + + fn set_fields(&self) -> impl Iterator { + self.props.prop_fields.iter().map(|pf| pf.to_field_setter()) + } + + fn impl_steps(&self) -> proc_macro2::TokenStream { + let Self { + builder_name, + props, + step_names, + .. + } = self; + let DerivePropsInput { + vis, + generics, + prop_fields, + .. + } = props; + + let (impl_generics, _, where_clause) = generics.split_for_impl(); + let mut fields_index = 0; + let mut token_stream = proc_macro2::TokenStream::new(); + + for (step, step_name) in step_names.iter().enumerate() { + let mut optional_fields = Vec::new(); + let mut required_field = None; + + if fields_index >= prop_fields.len() { + break; + } + + while let Some(pf) = prop_fields.get(fields_index) { + fields_index += 1; + if pf.is_required() { + required_field = Some(pf); + break; + } else { + optional_fields.push(pf); + } + } + + // Optional properties keep the builder on the current step + let current_step_arguments = to_arguments(generics, step_name.clone()); + let optional_prop_fn = optional_fields + .iter() + .map(|pf| pf.to_build_step_fn(builder_name, ¤t_step_arguments, vis)); + + // Required properties will advance the builder to the next step + let required_prop_fn = required_field.iter().map(|pf| { + let next_step_name = &step_names[step + 1]; + let next_step_arguments = to_arguments(generics, next_step_name.clone()); + pf.to_build_step_fn(builder_name, &next_step_arguments, vis) + }); + + token_stream.extend(quote! { + impl#impl_generics #builder_name<#current_step_arguments> #where_clause { + #(#optional_prop_fn)* + #(#required_prop_fn)* + } + }); + } + token_stream + } +} diff --git a/crates/macro/src/derive_props/field.rs b/crates/macro/src/derive_props/field.rs new file mode 100644 index 00000000000..b216b66db0e --- /dev/null +++ b/crates/macro/src/derive_props/field.rs @@ -0,0 +1,188 @@ +use super::generics::GenericArguments; +use proc_macro2::{Ident, Span}; +use quote::quote; +use std::cmp::{Ord, Ordering, PartialEq, PartialOrd}; +use std::convert::TryFrom; +use syn::parse::Result; +use syn::punctuated; +use syn::spanned::Spanned; +use syn::{Error, Field, Meta, MetaList, NestedMeta, Type, Visibility}; + +#[derive(Eq)] +pub struct PropField { + ty: Type, + name: Ident, + wrapped_name: Option, +} + +impl PropField { + /// All required property fields are wrapped in an `Option` + pub fn is_required(&self) -> bool { + self.wrapped_name.is_some() + } + + /// This step name is descriptive to help a developer realize they missed a required prop + pub fn to_step_name(&self, props_name: &Ident) -> Ident { + Ident::new( + &format!("{}_missing_required_prop_{}", props_name, self.name), + Span::call_site(), + ) + } + + /// Used to transform the `PropWrapper` struct into `Properties` + pub fn to_field_setter(&self) -> proc_macro2::TokenStream { + let name = &self.name; + if let Some(wrapped_name) = &self.wrapped_name { + quote! { + #name: self.wrapped.#wrapped_name.unwrap(), + } + } else { + quote! { + #name: self.wrapped.#name, + } + } + } + + /// Wrap all required props in `Option` + pub fn to_field_def(&self) -> proc_macro2::TokenStream { + let ty = &self.ty; + if let Some(wrapped_name) = &self.wrapped_name { + quote! { + #wrapped_name: ::std::option::Option<#ty>, + } + } else { + let name = &self.name; + quote! { + #name: #ty, + } + } + } + + /// All optional props must implement the `Default` trait + pub fn to_default_setter(&self) -> proc_macro2::TokenStream { + if let Some(wrapped_name) = &self.wrapped_name { + quote! { + #wrapped_name: ::std::default::Default::default(), + } + } else { + let name = &self.name; + quote! { + #name: ::std::default::Default::default(), + } + } + } + + /// Each field is set using a builder method + pub fn to_build_step_fn( + &self, + builder_name: &Ident, + generic_arguments: &GenericArguments, + vis: &Visibility, + ) -> proc_macro2::TokenStream { + let Self { + name, + ty, + wrapped_name, + } = self; + if let Some(wrapped_name) = wrapped_name { + quote! { + #[doc(hidden)] + #vis fn #name(mut self, #name: #ty) -> #builder_name<#generic_arguments> { + self.wrapped.#wrapped_name = ::std::option::Option::Some(#name); + #builder_name { + wrapped: self.wrapped, + _marker: ::std::marker::PhantomData, + } + } + } + } else { + quote! { + #[doc(hidden)] + #vis fn #name(mut self, #name: #ty) -> #builder_name<#generic_arguments> { + self.wrapped.#name = #name; + self + } + } + } + } + + // Detect the `#[props(required)]` attribute which denotes required fields + fn required_wrapper(named_field: &syn::Field) -> Result> { + let meta_list = if let Some(meta_list) = Self::find_props_meta_list(named_field) { + meta_list + } else { + return Ok(None); + }; + + let expected_required = syn::Error::new(meta_list.span(), "expected `props(required)`"); + let first_nested = if let Some(first_nested) = meta_list.nested.first() { + first_nested + } else { + return Err(expected_required); + }; + + let word_ident = match first_nested { + punctuated::Pair::End(NestedMeta::Meta(Meta::Word(ident))) => ident, + _ => return Err(expected_required), + }; + + if word_ident != "required" { + return Err(expected_required); + } + + if let Some(ident) = &named_field.ident { + Ok(Some(Ident::new( + &format!("{}_wrapper", ident), + Span::call_site(), + ))) + } else { + unreachable!() + } + } + + fn find_props_meta_list(field: &syn::Field) -> Option { + let meta_list = field + .attrs + .iter() + .find_map(|attr| match attr.parse_meta().ok()? { + Meta::List(meta_list) => Some(meta_list), + _ => None, + })?; + + if meta_list.ident == "props" { + Some(meta_list) + } else { + None + } + } +} + +impl TryFrom for PropField { + type Error = Error; + + fn try_from(field: Field) -> Result { + Ok(PropField { + wrapped_name: Self::required_wrapper(&field)?, + ty: field.ty, + name: field.ident.unwrap(), + }) + } +} + +impl PartialOrd for PropField { + fn partial_cmp(&self, other: &PropField) -> Option { + self.name.partial_cmp(&other.name) + } +} + +impl Ord for PropField { + fn cmp(&self, other: &PropField) -> Ordering { + self.name.cmp(&other.name) + } +} + +impl PartialEq for PropField { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} diff --git a/crates/macro/src/derive_props/generics.rs b/crates/macro/src/derive_props/generics.rs new file mode 100644 index 00000000000..6b983f5d04d --- /dev/null +++ b/crates/macro/src/derive_props/generics.rs @@ -0,0 +1,69 @@ +use proc_macro2::{Ident, Span}; +use syn::{ + punctuated::Punctuated, token::Colon2, GenericArgument, GenericParam, Generics, Path, + PathArguments, PathSegment, Token, TraitBound, TraitBoundModifier, Type, TypeParam, + TypeParamBound, TypePath, +}; + +/// Alias for a comma-separated list of `GenericArgument` +pub type GenericArguments = Punctuated; + +/// Converts `GenericParams` into `GenericArguments` and adds `type_ident` as a type arg +pub fn to_arguments(generics: &Generics, type_ident: Ident) -> GenericArguments { + let mut args: GenericArguments = Punctuated::new(); + args.extend(generics.params.iter().map(|param| match param { + GenericParam::Type(type_param) => new_generic_type_arg(type_param.ident.clone()), + GenericParam::Lifetime(lifetime_param) => { + GenericArgument::Lifetime(lifetime_param.lifetime.clone()) + } + _ => unimplemented!("const params are not supported in the derive macro"), + })); + args.push(new_generic_type_arg(type_ident)); + args +} + +/// Adds a new bounded `GenericParam` to a `Generics` +pub fn with_param_bounds(generics: &Generics, param_ident: Ident, param_bounds: Ident) -> Generics { + let mut new_generics = generics.clone(); + new_generics + .params + .push(new_param_bounds(param_ident, param_bounds)); + new_generics +} + +// Creates a `GenericArgument` from an `Ident` +fn new_generic_type_arg(ident: Ident) -> GenericArgument { + GenericArgument::Type(Type::Path(TypePath { + path: Path::from(ident), + qself: None, + })) +} + +// Creates a bounded `GenericParam` from two `Ident` +fn new_param_bounds(param_ident: Ident, param_bounds: Ident) -> GenericParam { + let mut path_segments: Punctuated = Punctuated::new(); + path_segments.push(PathSegment { + ident: param_bounds, + arguments: PathArguments::None, + }); + + let mut param_bounds: Punctuated = Punctuated::new(); + param_bounds.push(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: None, + path: Path { + leading_colon: None, + segments: path_segments, + }, + })); + + GenericParam::Type(TypeParam { + attrs: Vec::new(), + ident: param_ident, + colon_token: Some(Token![:](Span::call_site())), + bounds: param_bounds, + eq_token: None, + default: None, + }) +} diff --git a/crates/macro/src/derive_props/mod.rs b/crates/macro/src/derive_props/mod.rs new file mode 100644 index 00000000000..910840047a8 --- /dev/null +++ b/crates/macro/src/derive_props/mod.rs @@ -0,0 +1,86 @@ +mod builder; +mod field; +mod generics; +mod wrapper; + +use builder::PropsBuilder; +use field::PropField; +use proc_macro2::{Ident, Span}; +use quote::{quote, ToTokens}; +use std::convert::TryInto; +use syn::parse::{Parse, ParseStream, Result}; +use syn::{DeriveInput, Generics, Visibility}; +use wrapper::PropsWrapper; + +pub struct DerivePropsInput { + vis: Visibility, + generics: Generics, + props_name: Ident, + prop_fields: Vec, +} + +impl Parse for DerivePropsInput { + fn parse(input: ParseStream) -> Result { + let input: DeriveInput = input.parse()?; + let named_fields = match input.data { + syn::Data::Struct(data) => match data.fields { + syn::Fields::Named(fields) => fields.named, + _ => unimplemented!("only structs are supported"), + }, + _ => unimplemented!("only structs are supported"), + }; + + let mut prop_fields: Vec = named_fields + .into_iter() + .map(|f| f.try_into()) + .collect::>>()?; + + // Alphabetize + prop_fields.sort(); + + Ok(Self { + vis: input.vis, + props_name: input.ident, + generics: input.generics, + prop_fields, + }) + } +} + +impl ToTokens for DerivePropsInput { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { + generics, + props_name, + .. + } = self; + + // The wrapper is a new struct which wraps required props in `Option` + let wrapper_name = Ident::new(&format!("{}Wrapper", props_name), Span::call_site()); + let wrapper = PropsWrapper::new(&wrapper_name, &generics, &self.prop_fields); + tokens.extend(wrapper.into_token_stream()); + + // The builder will only build if all required props have been set + let builder_name = Ident::new(&format!("{}Builder", props_name), Span::call_site()); + let builder_step = Ident::new(&format!("{}BuilderStep", props_name), Span::call_site()); + let builder = PropsBuilder::new(&builder_name, &builder_step, &self, &wrapper_name); + let builder_generic_args = builder.first_step_generic_args(); + tokens.extend(builder.into_token_stream()); + + // The properties trait has a `builder` method which creates the props builder + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let properties = quote! { + impl#impl_generics ::yew::html::Properties for #props_name#ty_generics #where_clause { + type Builder = #builder_name<#builder_generic_args>; + + fn builder() -> Self::Builder { + #builder_name { + wrapped: ::std::boxed::Box::new(::std::default::Default::default()), + _marker: ::std::marker::PhantomData, + } + } + } + }; + tokens.extend(properties); + } +} diff --git a/crates/macro/src/derive_props/wrapper.rs b/crates/macro/src/derive_props/wrapper.rs new file mode 100644 index 00000000000..82cb3aab903 --- /dev/null +++ b/crates/macro/src/derive_props/wrapper.rs @@ -0,0 +1,65 @@ +use super::PropField; +use proc_macro2::Ident; +use quote::{quote, ToTokens}; +use syn::Generics; + +pub struct PropsWrapper<'a> { + wrapper_name: &'a Ident, + generics: &'a Generics, + prop_fields: &'a [PropField], +} + +impl ToTokens for PropsWrapper<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { + generics, + wrapper_name, + .. + } = self; + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let turbofish_generics = ty_generics.as_turbofish(); + + let wrapper_field_defs = self.field_defs(); + let wrapper_default_setters = self.default_setters(); + + let wrapper = quote! { + struct #wrapper_name#generics { + #(#wrapper_field_defs)* + } + + impl#impl_generics ::std::default::Default for #wrapper_name#ty_generics #where_clause { + fn default() -> Self { + #wrapper_name#turbofish_generics { + #(#wrapper_default_setters)* + } + } + } + }; + wrapper.to_tokens(tokens); + } +} + +impl<'a> PropsWrapper<'_> { + pub fn new( + name: &'a Ident, + generics: &'a Generics, + prop_fields: &'a [PropField], + ) -> PropsWrapper<'a> { + PropsWrapper { + wrapper_name: name, + generics, + prop_fields, + } + } +} + +impl PropsWrapper<'_> { + fn field_defs(&self) -> impl Iterator { + self.prop_fields.iter().map(|pf| pf.to_field_def()) + } + + fn default_setters(&self) -> impl Iterator { + self.prop_fields.iter().map(|pf| pf.to_default_setter()) + } +} diff --git a/tests/derive_props/fail.stderr b/tests/derive_props/fail.stderr index 0e3b14b3dfa..9f626a97d5b 100644 --- a/tests/derive_props/fail.stderr +++ b/tests/derive_props/fail.stderr @@ -12,7 +12,7 @@ error[E0277]: the trait bound `t1::Value: std::default::Default` is not satisfie | = note: required by `std::default::Default::default` -error[E0599]: no method named `build` found for type `t3::PropsBuilder` in the current scope +error[E0599]: no method named `build` found for type `t3::PropsBuilder` in the current scope --> $DIR/fail.rs:34:26 | 27 | #[derive(Properties)] @@ -21,7 +21,7 @@ error[E0599]: no method named `build` found for type `t3::PropsBuilder` in the current scope +error[E0599]: no method named `b` found for type `t4::PropsBuilder` in the current scope --> $DIR/fail.rs:48:26 | 40 | #[derive(Properties)] diff --git a/tests/derive_props/pass.rs b/tests/derive_props/pass.rs index 66691ad71dd..dc661769d37 100644 --- a/tests/derive_props/pass.rs +++ b/tests/derive_props/pass.rs @@ -64,4 +64,23 @@ mod t4 { } } +mod t5 { + use super::*; + + #[derive(Properties)] + pub struct Props<'a, T: Default + 'a> { + static_value: &'static str, + #[props(required)] + value: &'a T, + } + + fn optional_prop_generics_with_lifetime_should_work() { + Props::::builder().value(&String::from("")).build(); + Props::::builder() + .static_value("") + .value(&String::from("")) + .build(); + } +} + fn main() {} diff --git a/tests/macro/html-component-fail.stderr b/tests/macro/html-component-fail.stderr index e3d496ac17f..0462d2280d1 100644 --- a/tests/macro/html-component-fail.stderr +++ b/tests/macro/html-component-fail.stderr @@ -66,7 +66,7 @@ error[E0609]: no field `unknown` on type `ChildProperties` | = note: available fields are: `string`, `int` -error[E0599]: no method named `unknown` found for type `ChildPropertiesBuilder` in the current scope +error[E0599]: no method named `unknown` found for type `ChildPropertiesBuilder` in the current scope --> $DIR/html-component-fail.rs:42:29 | 5 | #[derive(Properties, PartialEq)] @@ -114,7 +114,7 @@ error[E0308]: mismatched types 47 | html! { }; | ^^^^ expected i32, found u32 -error[E0599]: no method named `string` found for type `ChildPropertiesBuilder` in the current scope +error[E0599]: no method named `string` found for type `ChildPropertiesBuilder` in the current scope --> $DIR/html-component-fail.rs:48:29 | 5 | #[derive(Properties, PartialEq)]