mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Add lifetime support to derive props macro
This commit is contained in:
parent
c9d5c70af0
commit
74373e6164
@ -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
|
||||
|
||||
@ -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<Ident>,
|
||||
}
|
||||
|
||||
impl TryFrom<Field> for PropField {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(field: Field) -> Result<Self> {
|
||||
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<PropField>,
|
||||
}
|
||||
|
||||
impl Parse for DerivePropsInput {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
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<PropField> = named_fields
|
||||
.into_iter()
|
||||
.map(|f| f.try_into())
|
||||
.collect::<Result<Vec<PropField>>>()?;
|
||||
|
||||
// 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<YEW_PROPS_BUILDER_STEP: #builder_step, #generic_params> #generic_where {
|
||||
wrapped: ::std::boxed::Box<#wrapped_name<#generic_types>>,
|
||||
_marker: ::std::marker::PhantomData<YEW_PROPS_BUILDER_STEP>,
|
||||
}
|
||||
|
||||
#(#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<Option<Ident>> {
|
||||
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<MetaList> {
|
||||
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<Ident> {
|
||||
let mut step_names: Vec<Ident> = 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<Item = impl ToTokens + '_> {
|
||||
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<Item = impl ToTokens + '_> {
|
||||
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<Item = impl ToTokens + '_> {
|
||||
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
|
||||
}
|
||||
}
|
||||
194
crates/macro/src/derive_props/builder.rs
Normal file
194
crates/macro/src/derive_props/builder.rs
Normal file
@ -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<Ident>,
|
||||
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<Ident> {
|
||||
let mut step_names: Vec<Ident> = 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<Item = impl ToTokens + '_> {
|
||||
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
|
||||
}
|
||||
}
|
||||
188
crates/macro/src/derive_props/field.rs
Normal file
188
crates/macro/src/derive_props/field.rs
Normal file
@ -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<Ident>,
|
||||
}
|
||||
|
||||
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<Option<Ident>> {
|
||||
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<MetaList> {
|
||||
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<Field> for PropField {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(field: Field) -> Result<Self> {
|
||||
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<Ordering> {
|
||||
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
|
||||
}
|
||||
}
|
||||
69
crates/macro/src/derive_props/generics.rs
Normal file
69
crates/macro/src/derive_props/generics.rs
Normal file
@ -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<GenericArgument, Token![,]>;
|
||||
|
||||
/// 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<PathSegment, Colon2> = Punctuated::new();
|
||||
path_segments.push(PathSegment {
|
||||
ident: param_bounds,
|
||||
arguments: PathArguments::None,
|
||||
});
|
||||
|
||||
let mut param_bounds: Punctuated<TypeParamBound, Token![+]> = 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)),
|
||||
bounds: param_bounds,
|
||||
eq_token: None,
|
||||
default: None,
|
||||
})
|
||||
}
|
||||
86
crates/macro/src/derive_props/mod.rs
Normal file
86
crates/macro/src/derive_props/mod.rs
Normal file
@ -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<PropField>,
|
||||
}
|
||||
|
||||
impl Parse for DerivePropsInput {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
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<PropField> = named_fields
|
||||
.into_iter()
|
||||
.map(|f| f.try_into())
|
||||
.collect::<Result<Vec<PropField>>>()?;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
65
crates/macro/src/derive_props/wrapper.rs
Normal file
65
crates/macro/src/derive_props/wrapper.rs
Normal file
@ -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<Item = impl ToTokens + '_> {
|
||||
self.prop_fields.iter().map(|pf| pf.to_field_def())
|
||||
}
|
||||
|
||||
fn default_setters(&self) -> impl Iterator<Item = impl ToTokens + '_> {
|
||||
self.prop_fields.iter().map(|pf| pf.to_default_setter())
|
||||
}
|
||||
}
|
||||
@ -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<t3::Props_value_is_required>` in the current scope
|
||||
error[E0599]: no method named `build` found for type `t3::PropsBuilder<t3::PropsBuilderStep_missing_required_prop_value>` 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<t3::Props
|
||||
34 | Props::builder().build();
|
||||
| ^^^^^
|
||||
|
||||
error[E0599]: no method named `b` found for type `t4::PropsBuilder<t4::Props_a_is_required>` in the current scope
|
||||
error[E0599]: no method named `b` found for type `t4::PropsBuilder<t4::PropsBuilderStep_missing_required_prop_a>` in the current scope
|
||||
--> $DIR/fail.rs:48:26
|
||||
|
|
||||
40 | #[derive(Properties)]
|
||||
|
||||
@ -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::<String>::builder().value(&String::from("")).build();
|
||||
Props::<String>::builder()
|
||||
.static_value("")
|
||||
.value(&String::from(""))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
|
||||
@ -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<ChildProperties_int_is_required>` in the current scope
|
||||
error[E0599]: no method named `unknown` found for type `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>` 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! { <ChildComponent int=0u32 /> };
|
||||
| ^^^^ expected i32, found u32
|
||||
|
||||
error[E0599]: no method named `string` found for type `ChildPropertiesBuilder<ChildProperties_int_is_required>` in the current scope
|
||||
error[E0599]: no method named `string` found for type `ChildPropertiesBuilder<ChildPropertiesBuilderStep_missing_required_prop_int>` in the current scope
|
||||
--> $DIR/html-component-fail.rs:48:29
|
||||
|
|
||||
5 | #[derive(Properties, PartialEq)]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user