diff --git a/Cargo.toml b/Cargo.toml index a2b9da3d..1cf85e4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ categories = ["rust-patterns"] proc-macro = true [dependencies] -syn = { version = "1", features = ["full", "extra-traits"] } -quote = "1" +either = "1" proc-macro2 = "1" +quote = "1" +regex = "1" +syn = { version = "1", features = ["full", "extra-traits"] } diff --git a/examples/default_generics.rs b/examples/default_generics.rs new file mode 100644 index 00000000..34574941 --- /dev/null +++ b/examples/default_generics.rs @@ -0,0 +1,36 @@ +use typed_builder::TypedBuilder; + +#[derive(TypedBuilder)] +pub struct Props<'a, OnInput: FnOnce(usize) -> usize = Box usize>> { + #[builder(default, setter(into))] + pub class: Option<&'a str>, + pub label: &'a str, + #[builder(setter(into))] + pub on_input: Option, +} + +#[derive(TypedBuilder)] +struct Foo { + #[builder(default = 12)] + x: T, +} + +#[allow(dead_code)] +#[derive(TypedBuilder)] +struct Bar { + t: T, + #[builder(default = 12)] + u: U, + v: (T, U, V), +} + +fn main() { + let props = Props::builder().label("label").on_input(|x: usize| x).build(); + assert_eq!(props.class, None); + assert_eq!(props.label, "label"); + assert_eq!((props.on_input.unwrap())(123), 123); + + assert_eq!(Foo::builder().build().x, 12); + + assert_eq!(Bar::builder().t("test").v(("t", 0, 3.14f64)).build().v.0, "t"); +} diff --git a/src/field_info.rs b/src/field_info.rs index 1be21a0e..adc8afd1 100644 --- a/src/field_info.rs +++ b/src/field_info.rs @@ -1,9 +1,13 @@ +use either::Either::*; use proc_macro2::{Span, TokenStream}; use quote::quote; +use std::collections::HashSet; use syn::parse::Error; use syn::spanned::Spanned; -use crate::util::{expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix}; +use crate::util::{ + empty_type, expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix, GenericDefault, +}; #[derive(Debug)] pub struct FieldInfo<'a> { @@ -12,19 +16,61 @@ pub struct FieldInfo<'a> { pub generic_ident: syn::Ident, pub ty: &'a syn::Type, pub builder_attr: FieldBuilderAttr, + pub default_ty: Option<(syn::Type, syn::Expr)>, + pub used_default_generic_idents: HashSet, } impl<'a> FieldInfo<'a> { - pub fn new(ordinal: usize, field: &syn::Field, field_defaults: FieldBuilderAttr) -> Result { + pub fn new( + ordinal: usize, + field: &'a syn::Field, + field_defaults: FieldBuilderAttr, + generic_defaults: &[GenericDefault], + ) -> Result { if let Some(ref name) = field.ident { - FieldInfo { + let mut field_info = FieldInfo { ordinal, name, generic_ident: syn::Ident::new(&format!("__{}", strip_raw_ident_prefix(name.to_string())), Span::call_site()), ty: &field.ty, builder_attr: field_defaults.with(&field.attrs)?, + default_ty: None, + used_default_generic_idents: HashSet::default(), } - .post_process() + .post_process()?; + + let mut ty_includes_params_without_defaults = false; + let mut ty_includes_params_with_defaults = false; + let ty = &field.ty; + let mut ty_str = format!("{}", quote! { #ty }); + for (generic_param, regular_expression, default_type) in generic_defaults.iter() { + if regular_expression.is_match(&ty_str) { + match default_type.as_ref() { + Some(default_type) => { + ty_includes_params_with_defaults = true; + ty_str = regular_expression.replace(&ty_str, default_type).into(); + match generic_param { + Left(type_param) => field_info.used_default_generic_idents.insert(type_param.ident.clone()), + Right(const_param) => field_info.used_default_generic_idents.insert(const_param.ident.clone()), + }; + } + None => { + ty_includes_params_without_defaults = true; + } + } + } + } + if !ty_includes_params_without_defaults && ty_includes_params_with_defaults { + if let Some(default_expr) = field_info.builder_attr.default.as_ref() { + use std::str::FromStr; + field_info.default_ty = Some(( + syn::parse(TokenStream::from_str(&format!("({ty_str},)"))?.into())?, + syn::parse(quote! { (#default_expr,) }.into())?, + )); + } + } + + Ok(field_info) } else { Err(Error::new(field.span(), "Nameless field in struct")) } @@ -94,6 +140,22 @@ impl<'a> FieldInfo<'a> { } Ok(self) } + + pub fn default_type(&self) -> syn::Type { + if let Some((ty, _)) = self.default_ty.as_ref() { + ty.clone() + } else { + empty_type() + } + } + + pub fn default_expr(&self) -> syn::Expr { + if let Some((_, expr)) = self.default_ty.as_ref() { + expr.clone() + } else { + syn::parse(quote!(()).into()).unwrap() + } + } } #[derive(Debug, Default, Clone)] diff --git a/src/lib.rs b/src/lib.rs index 7367d718..3bf0be7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,7 +179,7 @@ fn impl_my_derive(ast: &syn::DeriveInput) -> Result { let fields = quote!(#(#fields)*).into_iter(); let required_fields = struct_info .included_fields() - .filter(|f| f.builder_attr.default.is_none()) + .filter(|f| f.builder_attr.default.is_none() && f.default_ty.is_none()) .map(|f| struct_info.required_field_impl(f)) .collect::>(); let build_method = struct_info.build_method_impl(); diff --git a/src/struct_info.rs b/src/struct_info.rs index 9c43d61c..a29e32ca 100644 --- a/src/struct_info.rs +++ b/src/struct_info.rs @@ -1,11 +1,14 @@ +use either::Either::*; use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; +use regex::Regex; use syn::parse::Error; +use syn::punctuated::Punctuated; use crate::field_info::{FieldBuilderAttr, FieldInfo}; use crate::util::{ - empty_type, empty_type_tuple, expr_to_single_string, make_punctuated_single, modify_types_generics_hack, - path_to_single_string, strip_raw_ident_prefix, type_tuple, + empty_type_tuple, expr_to_single_string, expr_tuple, make_punctuated_single, modify_types_generics_hack, + path_to_single_string, strip_raw_ident_prefix, type_tuple, GenericDefault, }; #[derive(Debug)] @@ -14,6 +17,14 @@ pub struct StructInfo<'a> { pub name: &'a syn::Ident, pub generics: &'a syn::Generics, pub fields: Vec>, + // all generics, just with default types removed + pub generics_without_defaults: syn::Generics, + /// equivalent of a TyGenerics struct where TypeParams are replaced by their defaults if provided + pub ty_generics_with_defaults: Punctuated, + /// only generics which had no defaults specified + pub no_default_generics: syn::Generics, + /// map of word matching regex patterns for TypeParam idents to their corresponding default type + pub generic_defaults: Vec, pub builder_attr: TypeBuilderAttr, pub builder_name: syn::Ident, @@ -29,18 +40,137 @@ impl<'a> StructInfo<'a> { pub fn new(ast: &'a syn::DeriveInput, fields: impl Iterator) -> Result, Error> { let builder_attr = TypeBuilderAttr::new(&ast.attrs)?; let builder_name = strip_raw_ident_prefix(format!("{}Builder", ast.ident)); + + let mut generics_without_defaults = ast.generics.clone(); + generics_without_defaults.params = generics_without_defaults + .params + .into_iter() + .map(|param| match param { + syn::GenericParam::Type(type_param) => syn::GenericParam::Type(syn::TypeParam { + attrs: type_param.attrs, + ident: type_param.ident, + colon_token: type_param.colon_token, + bounds: type_param.bounds, + eq_token: None, + default: None, + }), + syn::GenericParam::Const(const_param) => syn::GenericParam::Const(syn::ConstParam { + attrs: const_param.attrs, + const_token: const_param.const_token, + ident: const_param.ident, + colon_token: const_param.colon_token, + ty: const_param.ty, + eq_token: None, + default: None, + }), + param => param, + }) + .collect(); + + let ty_generics_with_defaults: Punctuated<_, syn::token::Comma> = ast + .generics + .params + .clone() + .into_iter() + .map::, _>(|param| match param { + syn::GenericParam::Type(type_param) => match type_param.default { + Some(default) => syn::parse(proc_macro::TokenStream::from(quote!(#default))), + None => { + let ident = type_param.ident; + syn::parse(proc_macro::TokenStream::from(quote!(#ident))) + } + }, + syn::GenericParam::Lifetime(syn::LifetimeDef { lifetime, .. }) => { + syn::parse(proc_macro::TokenStream::from(quote!(#lifetime))) + } + syn::GenericParam::Const(const_param) => match const_param.default { + Some(default) => syn::parse(proc_macro::TokenStream::from(quote!(#default))), + None => { + let ident = const_param.ident; + syn::parse(proc_macro::TokenStream::from(quote!(#ident))) + } + }, + }) + .collect::>()?; + + let mut no_default_generics = ast.generics.clone(); + let mut generic_defaults = Vec::::default(); + no_default_generics.params = no_default_generics + .params + .into_iter() + .filter_map(|param| match param { + syn::GenericParam::Type(type_param) => match type_param.default.clone() { + Some(default) => { + let ident = &type_param.ident; + let regular_expression = Regex::new(format!(r#"\b{}\b"#, quote!(#ident)).trim()).expect(&format!( + "unable to replace generic parameter `{}`, not a matchable regex pattern", + format!("{}", quote!(#type_param)) + )); + generic_defaults.push(( + Left(type_param), + regular_expression, + Some(format!("{}", quote!(#default)).trim().to_string()), + )); + None + } + None => { + generic_defaults.push(( + Left(type_param.clone()), + Regex::new(format!(r#"\b{}\b"#, quote!(#type_param)).trim()).expect(&format!( + "unable to replace generic parameter `{}`, not a matchable regex pattern", + format!("{}", quote!(#type_param)) + )), + None, + )); + Some(syn::GenericParam::Type(type_param)) + } + }, + syn::GenericParam::Const(const_param) => match const_param.default.clone() { + Some(default) => { + let ident = &const_param.ident; + let regular_expression = Regex::new(format!(r#"\b{}\b"#, quote!(#ident)).trim()).expect(&format!( + "unable to replace generic parameter `{}`, not a matchable regex pattern", + format!("{}", quote!(#const_param)) + )); + generic_defaults.push(( + Right(const_param), + regular_expression, + Some(format!("{}", quote!(#default)).trim().to_string()), + )); + None + } + None => { + generic_defaults.push(( + Right(const_param.clone()), + Regex::new(format!(r#"\b{}\b"#, quote!(#const_param)).trim()).expect(&format!( + "unable to replace generic parameter `{}`, not a matchable regex pattern", + format!("{}", quote!(#const_param)) + )), + None, + )); + Some(syn::GenericParam::Const(const_param)) + } + }, + param => Some(param), + }) + .collect(); + Ok(StructInfo { vis: &ast.vis, name: &ast.ident, generics: &ast.generics, fields: fields .enumerate() - .map(|(i, f)| FieldInfo::new(i, f, builder_attr.field_defaults.clone())) + .map(|(i, f)| FieldInfo::new(i, f, builder_attr.field_defaults.clone(), &generic_defaults)) .collect::>()?, - builder_attr, builder_name: syn::Ident::new(&builder_name, proc_macro2::Span::call_site()), conversion_helper_trait_name: syn::Ident::new(&format!("{}_Optional", builder_name), proc_macro2::Span::call_site()), core: syn::Ident::new(&format!("{}_core", builder_name), proc_macro2::Span::call_site()), + builder_attr, + generics_without_defaults, + ty_generics_with_defaults, + no_default_generics, + generic_defaults, }) } @@ -50,6 +180,68 @@ impl<'a> StructInfo<'a> { generics } + fn modify_generics_alter_if_used_default_generic( + &self, + mut mutator: F, + field: &FieldInfo, + ) -> syn::Generics { + let mut generics = self.generics.clone(); + generics.params.iter_mut().for_each(|param| match param { + syn::GenericParam::Type(ref mut type_param) => { + if type_param.default.is_some() && field.used_default_generic_idents.contains(&type_param.ident) { + type_param.ident = format_ident_target_generic_default(&type_param.ident); + } + } + syn::GenericParam::Const(ref mut const_param) => { + if const_param.default.is_some() && field.used_default_generic_idents.contains(&const_param.ident) { + const_param.ident = format_ident_target_generic_default(&const_param.ident); + } + } + _ => {} + }); + mutator(&mut generics); + generics + } + + fn modify_generics_with_no_defaults(&self, mut mutator: F) -> syn::Generics { + let mut generics = self.generics_without_defaults.clone(); + mutator(&mut generics); + generics + } + + fn ty_generics_with_defaults_except_field(&self, field: &FieldInfo) -> Result, Error> { + self.generics + .params + .clone() + .into_iter() + .map::, _>(|param| match param { + syn::GenericParam::Type(type_param) => match field.used_default_generic_idents.contains(&type_param.ident) { + true => { + let ident = format_ident_target_generic_default(&type_param.ident); + syn::parse(proc_macro::TokenStream::from(quote!(#ident))) + } + false => { + let ident = &type_param.ident; + syn::parse(proc_macro::TokenStream::from(quote!(#ident))) + } + }, + syn::GenericParam::Lifetime(syn::LifetimeDef { lifetime, .. }) => { + syn::parse(proc_macro::TokenStream::from(quote!(#lifetime))) + } + syn::GenericParam::Const(const_param) => match field.used_default_generic_idents.contains(&const_param.ident) { + true => { + let ident = format_ident_target_generic_default(&const_param.ident); + syn::parse(proc_macro::TokenStream::from(quote!(#ident))) + } + false => { + let ident = &const_param.ident; + syn::parse(proc_macro::TokenStream::from(quote!(#ident))) + } + }, + }) + .collect::>() + } + pub fn builder_creation_impl(&self) -> Result { let StructInfo { ref vis, @@ -57,16 +249,24 @@ impl<'a> StructInfo<'a> { ref builder_name, .. } = *self; - let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); + let (impl_generics, ty_generics, where_clause) = self.no_default_generics.split_for_impl(); let all_fields_param = syn::GenericParam::Type(syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into()); - let b_generics = self.modify_generics(|g| { + let b_generics = self.modify_generics_with_no_defaults(|g| { g.params.insert(0, all_fields_param.clone()); }); - let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type())); - let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| { - args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into())); - }); + let expr_empties_tuple = expr_tuple(self.included_fields().map(|f| f.default_expr())); + let ty_empties_tuple = type_tuple(self.included_fields().map(|f| f.default_type())); + + let mut ty_generics_with_defaults = self.ty_generics_with_defaults.clone(); + ty_generics_with_defaults.insert(0, syn::GenericArgument::Type(ty_empties_tuple.clone().into())); + + let generics_with_empty = syn::AngleBracketedGenericArguments { + colon2_token: None, + lt_token: Default::default(), + args: ty_generics_with_defaults, + gt_token: Default::default(), + }; let phantom_generics = self.generics.params.iter().map(|param| match param { syn::GenericParam::Lifetime(lifetime) => { let lifetime = &lifetime.lifetime; @@ -138,7 +338,7 @@ impl<'a> StructInfo<'a> { #[allow(dead_code, clippy::default_trait_access)] #vis fn builder() -> #builder_name #generics_with_empty { #builder_name { - fields: #empties_tuple, + fields: #expr_empties_tuple, phantom: ::core::default::Default::default(), } } @@ -172,17 +372,17 @@ impl<'a> StructInfo<'a> { #[doc(hidden)] #[allow(dead_code, non_camel_case_types, non_snake_case)] pub trait #trait_name { - fn into_value T>(self, default: F) -> T; + fn into_value Option>(self, default: F) -> T; } impl #trait_name for () { - fn into_value T>(self, default: F) -> T { - default() + fn into_value Option>(self, default: F) -> T { + default().unwrap() } } impl #trait_name for (T,) { - fn into_value T>(self, _: F) -> T { + fn into_value Option>(self, _: F) -> T { self.0 } } @@ -207,7 +407,8 @@ impl<'a> StructInfo<'a> { ty: field_type, .. } = field; - let mut ty_generics: Vec = self + + let mut target_generics: Vec = self .generics .params .iter() @@ -223,9 +424,17 @@ impl<'a> StructInfo<'a> { } }) .collect(); + + let mut ty_generics: Vec = if !field.used_default_generic_idents.is_empty() { + self.ty_generics_with_defaults_except_field(field)? + } else { + target_generics.clone() + }; + let mut target_generics_tuple = empty_type_tuple(); let mut ty_generics_tuple = empty_type_tuple(); - let generics = self.modify_generics(|g| { + + let modify_generics_callback = |g: &mut syn::Generics| { let index_after_lifetime_in_generics = g .params .iter() @@ -233,7 +442,7 @@ impl<'a> StructInfo<'a> { .count(); for f in self.included_fields() { if f.ordinal == field.ordinal { - ty_generics_tuple.elems.push_value(empty_type()); + ty_generics_tuple.elems.push_value(f.default_type()); target_generics_tuple.elems.push_value(f.tuplized_type_ty_param()); } else { g.params.insert(index_after_lifetime_in_generics, f.generic_ty_param()); @@ -244,8 +453,13 @@ impl<'a> StructInfo<'a> { ty_generics_tuple.elems.push_punct(Default::default()); target_generics_tuple.elems.push_punct(Default::default()); } - }); - let mut target_generics = ty_generics.clone(); + }; + let generics = if !field.used_default_generic_idents.is_empty() { + self.modify_generics_alter_if_used_default_generic(modify_generics_callback, field) + } else { + self.modify_generics(modify_generics_callback) + }; + let index_after_lifetime_in_generics = target_generics .iter() .filter(|arg| matches!(arg, syn::GenericArgument::Lifetime(_))) @@ -280,6 +494,34 @@ impl<'a> StructInfo<'a> { (quote!(#arg_type), quote!(#field_name)) }; + let fn_generics = { + let ty_str = format!("{arg_type}"); + let mut generic_params = vec![]; + for (generic_param, regular_expression, default_type) in self.generic_defaults.iter() { + if default_type.is_some() && regular_expression.is_match(&ty_str) { + match generic_param { + Left(type_param) => { + let mut type_param = type_param.clone(); + type_param.eq_token = None; + type_param.default = None; + generic_params.push(quote!(#type_param)); + } + Right(const_param) => { + let mut const_param = const_param.clone(); + const_param.eq_token = None; + const_param.default = None; + generic_params.push(quote!(#const_param)); + } + } + } + } + if generic_params.is_empty() { + quote!() + } else { + quote!(<#(#generic_params),*>) + } + }; + let (param_list, arg_expr) = if field.builder_attr.setter.strip_bool.is_some() { (quote!(), quote!(true)) } else if let Some(transform) = &field.builder_attr.setter.transform { @@ -292,42 +534,52 @@ impl<'a> StructInfo<'a> { (quote!(#field_name: #arg_type), arg_expr) }; - let repeated_fields_error_type_name = syn::Ident::new( - &format!( - "{}_Error_Repeated_field_{}", - builder_name, - strip_raw_ident_prefix(field_name.to_string()) - ), - proc_macro2::Span::call_site(), - ); - let repeated_fields_error_message = format!("Repeated field {}", field_name); + // repeated field impl cannot exist if field includes use of a default type because of overlapping impls + let repeated_field_impl = if field.used_default_generic_idents.is_empty() { + let repeated_fields_error_type_name = syn::Ident::new( + &format!( + "{}_Error_Repeated_field_{}", + builder_name, + strip_raw_ident_prefix(field_name.to_string()) + ), + proc_macro2::Span::call_site(), + ); + let repeated_fields_error_message = format!("Repeated field {}", field_name); + quote! { + #[doc(hidden)] + #[allow(dead_code, non_camel_case_types, non_snake_case)] + pub enum #repeated_fields_error_type_name {} + + #[doc(hidden)] + #[allow(dead_code, non_camel_case_types, missing_docs)] + impl #impl_generics #builder_name < #( #target_generics ),* > #where_clause { + #[deprecated( + note = #repeated_fields_error_message + )] + pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > { + self + } + } + } + } else { + quote! {} + }; Ok(quote! { #[allow(dead_code, non_camel_case_types, missing_docs)] impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause { #doc - pub fn #field_name (self, #param_list) -> #builder_name < #( #target_generics ),* > { + pub fn #field_name #fn_generics (self, #param_list) -> #builder_name < #( #target_generics ),* > { let #field_name = (#arg_expr,); let ( #(#descructuring,)* ) = self.fields; #builder_name { fields: ( #(#reconstructing,)* ), - phantom: self.phantom, + phantom: ::core::default::Default::default(), } } } - #[doc(hidden)] - #[allow(dead_code, non_camel_case_types, non_snake_case)] - pub enum #repeated_fields_error_type_name {} - #[doc(hidden)] - #[allow(dead_code, non_camel_case_types, missing_docs)] - impl #impl_generics #builder_name < #( #target_generics ),* > #where_clause { - #[deprecated( - note = #repeated_fields_error_message - )] - pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > { - self - } - } + + #repeated_field_impl }) } @@ -380,7 +632,7 @@ impl<'a> StructInfo<'a> { // `f`'s `build` method will warn, since it appears earlier in the argument list. builder_generics_tuple.elems.push_value(f.tuplized_type_ty_param()); } else if f.ordinal == field.ordinal { - builder_generics_tuple.elems.push_value(empty_type()); + builder_generics_tuple.elems.push_value(f.default_type()); } else { // `f` appears later in the argument list after `field`, so if they are both missing we will // show a warning for `field` and not for `f` - which means this warning should appear whether @@ -500,8 +752,10 @@ impl<'a> StructInfo<'a> { if let Some(ref default) = field.builder_attr.default { if field.builder_attr.setter.skip.is_some() { quote!(let #name = #default;) + } else if !field.used_default_generic_idents.is_empty() { + quote!(let #name = #helper_trait_name::into_value(#name, || None);) } else { - quote!(let #name = #helper_trait_name::into_value(#name, || #default);) + quote!(let #name = #helper_trait_name::into_value(#name, || Some(#default));) } } else { quote!(let #name = #name.0;) @@ -707,3 +961,7 @@ impl TypeBuilderAttr { } } } + +fn format_ident_target_generic_default(ident: &syn::Ident) -> syn::Ident { + format_ident!("{ident}__") +} diff --git a/src/util.rs b/src/util.rs index ae3306d8..4c7bf5df 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,13 @@ -use quote::ToTokens; +use either::Either; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use regex::Regex; + +// tuple of: +// - TypeParam or ConstParam +// - regex pattern of format /\b$ident\b/ where $ident is the ident of the first tuple item +// - default type if provided, formatted and trimmed as a String +pub type GenericDefault = (Either, Regex, Option); pub fn path_to_single_string(path: &syn::Path) -> Option { if path.leading_colon.is_some() { @@ -55,6 +64,10 @@ pub fn type_tuple(elems: impl Iterator) -> syn::TypeTuple { result } +pub fn expr_tuple(elems: impl Iterator) -> TokenStream { + quote! { (#(#elems,)*) } +} + pub fn empty_type_tuple() -> syn::TypeTuple { syn::TypeTuple { paren_token: Default::default(), diff --git a/tests/tests.rs b/tests/tests.rs index 0cb43a56..45ee5e02 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -331,7 +331,7 @@ fn test_builder_type_with_default_on_generic_type() { assert!(Types::builder().x(()).y(()).build() == Types { x: (), y: () }); #[derive(PartialEq, TypedBuilder)] - struct TypeAndLifetime<'a, X, Y: Default, Z = usize> { + struct TypeAndLifetime<'a, X, Y: Default, Z> { x: X, y: Y, z: &'a Z, @@ -360,7 +360,7 @@ fn test_builder_type_with_default_on_generic_type() { } // compile test if rustc can infer type for `z` and `m` - Foo::<(), _, _, f64>::builder().x(()).y(&a).z_default().m(1.0).build(); + Foo::<(), _, _, _>::builder().x(()).y(&a).z_default().m(1.0f64).build(); Foo::<(), _, _, _>::builder().x(()).y(&a).z_default().m_default().build(); assert!( @@ -574,3 +574,41 @@ fn test_build_method() { assert!(Foo::builder().x(1).__build() == Foo { x: 1 }); } + +#[test] +fn test_struct_generic_defaults() { + #[derive(TypedBuilder)] + pub struct Props<'a, OnInput: FnOnce(usize) -> usize = Box usize>> { + #[builder(default, setter(into))] + pub class: Option<&'a str>, + pub label: &'a str, + #[builder(default = Some(Box::new(|x: usize| x + 1)), setter(strip_option))] + pub on_input: Option, + } + + let _: PropsBuilder<((), (), (Option usize>>,)), Box usize>> = Props::builder(); + + let props = Props::builder().label("label").on_input(|x: usize| x).build(); + assert_eq!(props.class, None); + assert_eq!(props.label, "label"); + assert_eq!((props.on_input.unwrap())(123), 123); + + #[derive(TypedBuilder)] + struct Foo { + #[builder(default = 12)] + x: T, + } + + assert_eq!(Foo::builder().build().x, 12); + + #[allow(dead_code)] + #[derive(TypedBuilder)] + struct Bar { + t: T, + #[builder(default = 12)] + u: U, + v: (T, U, V), + } + + assert_eq!(Bar::builder().t("test").v(("t", 0, 3.14f64)).build().v.0, "t"); +}