Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specify default field value for field with type that uses generic parameters with defaults #73

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
36 changes: 36 additions & 0 deletions examples/default_generics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use typed_builder::TypedBuilder;

#[derive(TypedBuilder)]
pub struct Props<'a, OnInput: FnOnce(usize) -> usize = Box<dyn FnOnce(usize) -> usize>> {
#[builder(default, setter(into))]
pub class: Option<&'a str>,
pub label: &'a str,
#[builder(setter(into))]
pub on_input: Option<OnInput>,
}

#[derive(TypedBuilder)]
struct Foo<T = usize> {
#[builder(default = 12)]
x: T,
}

#[allow(dead_code)]
#[derive(TypedBuilder)]
struct Bar<T, U = usize, V = usize> {
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");
}
70 changes: 66 additions & 4 deletions src/field_info.rs
Original file line number Diff line number Diff line change
@@ -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> {
Expand All @@ -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<syn::Ident>,
}

impl<'a> FieldInfo<'a> {
pub fn new(ordinal: usize, field: &syn::Field, field_defaults: FieldBuilderAttr) -> Result<FieldInfo, Error> {
pub fn new(
ordinal: usize,
field: &'a syn::Field,
field_defaults: FieldBuilderAttr,
generic_defaults: &[GenericDefault],
) -> Result<Self, Error> {
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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you doing this to determine if the type depends on the generic parameter? I'd prefer doing this with https://docs.rs/syn/latest/syn/visit/index.html

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"))
}
Expand Down Expand Up @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
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::<Vec<_>>();
let build_method = struct_info.build_method_impl();
Expand Down