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

Optionally transform Extend fields to add items one by one #46

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
486480b
Rudimentarily adjusted parser to accept `strip_extend` with optional …
Tamschi Jan 16, 2021
c824836
Implement `strip_extend` proof of concept
Tamschi Jan 17, 2021
03e0e58
Rework `strip_extend` into `strip_collection`, which has stricter typ…
Tamschi Jan 18, 2021
b19a01b
Use `FromIterator` to create initial stripped collection iff neither …
Tamschi Jan 18, 2021
4376566
Accept any valid `Extend::extend` parameter in repeat setter calls
Tamschi Jan 18, 2021
8e4b301
Accept e.g. `strip_collection(from_first = |first| vec![first])` inst…
Tamschi Jan 22, 2021
7635462
Merge branch 'master' into extend-field-buildup
Tamschi Feb 8, 2021
a3d3e31
Rework `strip_collection` into `extend`, allow extending with an iter…
Tamschi Feb 4, 2021
c9bba54
Accept any matching `IntoIterator` instead of just matching `Iterator…
Tamschi Feb 8, 2021
586d3dc
Add some `extend` tests, fix issues with type inference and improve c…
Tamschi Feb 13, 2021
23ca53a
Ignore field default in automatic extend initialisers
Tamschi Feb 13, 2021
094a827
Add generics support in combination with `extend`
Tamschi Feb 13, 2021
4a2afb0
Remove extra parentheses
Tamschi Feb 13, 2021
a3006a1
Support combining `strip_option` and `extend` on the same field
Tamschi Feb 13, 2021
9bb9dbf
cargo +nightly fmt
Tamschi Feb 13, 2021
4c67cf4
Assert `strip_option` flag inside `type_from_inside_option`
Tamschi Feb 13, 2021
c8abc4d
Mention single item setter name in `early_build_error_message`
Tamschi Feb 14, 2021
8cc816a
Merge branch 'master' into extend-field-buildup
Tamschi Jun 22, 2021
3a8ff5f
Merge remote-tracking branch 'upstream/master' into extend-field-buildup
Tamschi Nov 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions examples/example.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
use std::{collections::HashMap, iter};

use typed_builder::TypedBuilder;

macro_rules! extend {
[$init:expr; $($expr:expr),*$(,)?] => {{
let mut e = $init;
$(e.extend(iter::once($expr));)*
e
}};
}

#[derive(PartialEq, TypedBuilder)]
struct Foo {
// Mandatory Field:
Expand All @@ -13,16 +23,73 @@ struct Foo {
// Or you can set the default
#[builder(default = 20)]
z: i32,

// #[builder(default)] without parameter - don't require this field
// #[builder(setter(strip_collection))] without parameter - start with the default and extend from there
#[builder(default, setter(strip_collection))]
v0: Vec<i32>,

// No `default`: This field must be set at least once.
// You can explicitly create the collection from the first item (but this is not required even without `default`).
#[builder(setter(strip_collection(from_first = |first| vec![first])))]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think from_first is more clear than initializer here, and will nicely pair with from_iter once added.

I'm concerned about what to default to if just one of the parameters is present, or with empty parentheses.
My current hunch is that initialisers missing from explicit parentheses shouldn't be generated at all, but also to accept from_first and from_iter without assignment to generate the default each. This would mirror how default works as builder(…) parameter.

I could also add support for !from_first and !from_iter to delete them again once set, but that feels a bit odd to me.
Is there a specific reason this is supported on the fields? The application I can think of is for overrides when combined with a macro, but I'm just guessing here 🤔

Copy link
Owner

Choose a reason for hiding this comment

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

Is from_first without arguments even meaningful? What would be the default?

As for from_iter - I think maybe it should be the default - as in, without an argument you won't even need to specify it. Instead of keeping the actual target collection we can keep I: Iterator, and each time we extend with the builder, we can make an std::iter::Chain. We can still specify from_iter = |it| ... if FromIterator is not available for that collection or if for whatever reason we need custom behavior.

v1: Vec<i32>,

// Other `Extend` types are also supported.
#[builder(default, setter(strip_collection))]
h: HashMap<i32, i32>,
}

fn main() {
assert!(Foo::builder().x(1).y(2).z(3).build() == Foo { x: 1, y: Some(2), z: 3 });
assert!(
Foo::builder().x(1).y(2).z(3).v0(4).v1(5).h((6, 7)).build()
== Foo {
x: 1,
y: Some(2),
z: 3,
v0: vec![4],
v1: vec![5],
h: extend![HashMap::new(); (6, 7)],
}
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll split this and put it into a separate file. You're right that it's just too much at once.


// Change the order of construction:
assert!(Foo::builder().z(1).x(2).y(3).build() == Foo { x: 2, y: Some(3), z: 1 });
assert!(
Foo::builder().z(1).x(2).h((3, 4)).v1(5).v0(6).y(7).build()
== Foo {
x: 2,
y: Some(7),
z: 1,
v0: vec![6],
v1: vec![5],
h: extend![HashMap::new(); (3, 4)],
}
);

// Optional fields are optional:
assert!(Foo::builder().x(1).build() == Foo { x: 1, y: None, z: 20 });
assert!(
Foo::builder().x(1).v1(2).build()
== Foo {
x: 1,
y: None,
z: 20,
v0: vec![],
v1: vec![2],
h: HashMap::new(),
}
);

// Extend fields can be set multiple times:
assert!(
Foo::builder().x(1).v0(2).v0(3).v0(4).v1(5).v1(6).h((7, 8)).h((9, 10)).build()
== Foo {
x: 1,
y: None,
z: 20,
v0: vec![3, 4],
v1: vec![5, 6],
h: extend![HashMap::new(); (7, 8), (9, 10)],
}
);

// This will not compile - because we did not set x:
// Foo::builder().build();
Expand Down
84 changes: 82 additions & 2 deletions src/field_info.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use proc_macro2::TokenStream;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::parse::Error;
use syn::spanned::Spanned;
Expand Down Expand Up @@ -87,9 +87,22 @@ pub struct SetterSettings {
pub doc: Option<syn::Expr>,
pub skip: bool,
pub auto_into: bool,
pub strip_collection: Option<StripCollection>,
pub strip_option: bool,
}

#[derive(Debug, Clone)]
pub struct StripCollection {
pub keyword_span: Span,
pub from_first: Option<FromFirst>,
}

#[derive(Debug, Clone)]
pub struct FromFirst {
pub keyword_span: Span,
pub closure: syn::ExprClosure,
}

impl FieldBuilderAttr {
pub fn with(mut self, attrs: &[syn::Attribute]) -> Result<Self, Error> {
let mut skip_tokens = None;
Expand Down Expand Up @@ -232,6 +245,58 @@ impl SetterSettings {
_ => Err(Error::new_spanned(&assign, format!("Unknown parameter {:?}", name))),
}
}
syn::Expr::Call(call) => {
let name =
expr_to_single_string(&call.func).ok_or_else(|| Error::new_spanned(&call.func, "Expected identifier"))?;
match name.as_str() {
"strip_collection" => {
if self.strip_collection.is_some() {
Err(Error::new(
call.span(),
"Illegal setting - field is already calling extend(...) with the argument",
))
} else if let Some(attr) = call.attrs.first() {
Err(Error::new_spanned(attr, "Unexpected attribute"))
} else {
let mut strip_collection = StripCollection {
keyword_span: name.span(),
from_first: None,
};
for arg in call.args {
match arg {
syn::Expr::Assign(assign) => {
let name = expr_to_single_string(&assign.left)
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
match name.as_str() {
"from_first" => match *assign.right {
syn::Expr::Closure(closure) => {
strip_collection.from_first = Some(FromFirst {
keyword_span: assign.left.span(),
closure,
})
}
other => {
return Err(Error::new_spanned(other, "Expected closure (|first| <...>)"))
}
},
_ => {
return Err(Error::new_spanned(
&assign.left,
format!("Unknown parameter {:?}", name),
))
}
}
}
_ => return Err(Error::new_spanned(arg, "Expected (<...>=<...>)")),
}
}
self.strip_collection = Some(strip_collection);
Ok(())
}
}
_ => Err(Error::new_spanned(&call.func, format!("Unknown parameter {:?}", name))),
}
}
syn::Expr::Path(path) => {
let name = path_to_single_string(&path.path).ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
macro_rules! handle_fields {
Expand All @@ -247,6 +312,17 @@ impl SetterSettings {
}
}
)*
"strip_collection" => {
if self.strip_collection.is_some() {
Err(Error::new(path.span(), "Illegal setting - field is already calling extend(...) with the argument"))
} else {
self.strip_collection = Some(StripCollection{
keyword_span: name.span(),
from_first: None
});
Ok(())
}
}
_ => Err(Error::new_spanned(
&path,
format!("Unknown setter parameter {:?}", name),
Expand Down Expand Up @@ -281,6 +357,10 @@ impl SetterSettings {
self.auto_into = false;
Ok(())
}
"strip_collection" => {
self.strip_collection = None;
Ok(())
}
"strip_option" => {
self.strip_option = false;
Ok(())
Expand All @@ -291,7 +371,7 @@ impl SetterSettings {
Err(Error::new_spanned(expr, "Expected simple identifier".to_owned()))
}
}
_ => Err(Error::new_spanned(expr, "Expected (<...>=<...>)")),
_ => Err(Error::new_spanned(expr, "Expected (<...>=<...>) or (<...>(<...>))")),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't sure about the placeholder syntax here, but I think I got "assignment or call" right.
Since ident and !ident are accepted too, this might need an update, though. I didn't change this for now since those cases both were already present.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe using Lookahead1 would make sense here, since that can generate a similar error message automatically. The syntax isn't as clean as the match statement here, though.

}
}
}
126 changes: 92 additions & 34 deletions src/struct_info.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use proc_macro2::TokenStream;
use quote::quote;
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned, ToTokens};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tend to use quote_spanned quite often even when it's not strictly necessary, since it tends to make for better error locations.

My earlier hack with #[forbid(unused_variables)] is gone, of course, but this should still flag some type/trait errors on the field itself or the matching attribute or attribute parameter.

use syn::parse::Error;
use syn::{self, Ident};

use crate::field_info::{FieldBuilderAttr, FieldInfo};
use crate::field_info::{FieldBuilderAttr, FieldInfo, FromFirst, StripCollection};
use crate::util::{
empty_type, empty_type_tuple, expr_to_single_string, make_punctuated_single, modify_types_generics_hack,
path_to_single_string, type_tuple,
Expand Down Expand Up @@ -197,19 +198,22 @@ impl<'a> StructInfo<'a> {
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
let StructInfo { ref builder_name, .. } = *self;

let descructuring = self.included_fields().map(|f| {
if f.ordinal == field.ordinal {
quote!(_)
} else {
let name = f.name;
quote!(#name)
}
});
let reconstructing = self.included_fields().map(|f| f.name);
let descructuring = self
.included_fields()
.map(|f| {
if f.ordinal == field.ordinal {
quote!(_)
} else {
let name = f.name;
quote!(#name)
}
})
.collect::<Vec<_>>();
let reconstructing = self.included_fields().map(|f| f.name).collect::<Vec<_>>();

let &FieldInfo {
name: ref field_name,
ty: ref field_type,
name: field_name,
ty: field_type,
..
} = field;
let mut ty_generics: Vec<syn::GenericArgument> = self
Expand Down Expand Up @@ -275,22 +279,88 @@ impl<'a> StructInfo<'a> {
} else {
field_type
};
let (arg_type, arg_expr) = if let Some(StripCollection {
ref keyword_span,
ref from_first,
}) = field.builder_attr.setter.strip_collection
{
let arg_expr = if let Some(FromFirst { keyword_span, closure }) = from_first {
// `Span::mixed_site()`-resolution suppresses `clippy::redundant_closure_call` on
// the generated code only.
quote_spanned!(keyword_span.resolved_at(Span::mixed_site())=> (#closure)(#field_name))
} else if let Some(default) = &field.builder_attr.default {
quote_spanned!(*keyword_span=> {
let mut collection: #arg_type = #default;
::core::iter::Extend::extend(&mut collection, ::core::iter::once(#field_name));
collection
})
} else {
quote_spanned!(*keyword_span=> ::<#arg_type as ::core::iter::FromIterator>::from_iter(::core::iter::once(#field_name)))
};
(quote!(<#arg_type as ::core::iter::IntoIterator>::Item), arg_expr)
} else {
(arg_type.to_token_stream(), field_name.to_token_stream())
};
let (arg_type, arg_expr) = if field.builder_attr.setter.auto_into {
(quote!(impl core::convert::Into<#arg_type>), quote!(#field_name.into()))
(quote!(impl core::convert::Into<#arg_type>), quote!(#arg_expr.into()))
} else {
(quote!(#arg_type), quote!(#field_name))
(arg_type, arg_expr)
};
let arg_expr = if field.builder_attr.setter.strip_option {
quote!(Some(#arg_expr))
} else {
arg_expr
};

let repeated_fields_error_type_name = syn::Ident::new(
&format!("{}_Error_Repeated_field_{}", builder_name, field_name),
proc_macro2::Span::call_site(),
);
let repeated_fields_error_message = format!("Repeated field {}", field_name);
let repeat_items = if let Some(StripCollection { keyword_span, .. }) = field.builder_attr.setter.strip_collection {
let field_hygienic = Ident::new(
// Changing the name here doesn't really matter, but makes it easier to debug with `cargo expand`.
&format!("{}_argument", field_name),
field_name.span().resolved_at(Span::mixed_site()),
);
quote_spanned! {keyword_span=>
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name < #( #target_generics ),* > #where_clause {
#doc
pub fn #field_name<Argument> (self, #field_name: Argument) -> #builder_name < #( #target_generics ),* >
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I haven't changed the argument name here yet, but I've been thinking that item for the single case and items when extending with an iterator might be more clear. That would also avoid the field_hygienic workaround.

The generic type parameter Argument is a bad choice on my part - I'll change it to Item once I get back to this.

// Making this repeat setter here further generic has a potential performance benefit:
// Many collections are also `Extend<&T>` where `T: Copy`. Calling this overload will then
// copy the value direction into the collection instead of through a stack temporary.
where #field_type: ::core::iter::Extend<Argument>,
{
let #field_hygienic = #field_name;
let ( #(#reconstructing,)* ) = self.fields;
let mut #field_name = #field_name;
::core::iter::Extend::extend(&mut #field_name.0, ::core::iter::once(#field_hygienic));
#builder_name {
fields: ( #(#reconstructing,)* ),
_phantom: self._phantom,
}
}
}
}
} else {
let repeated_fields_error_type_name = syn::Ident::new(
&format!("{}_Error_Repeated_field_{}", builder_name, field_name),
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
}
}
}
};

Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
Expand All @@ -305,19 +375,7 @@ impl<'a> StructInfo<'a> {
}
}
}
#[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
}
}
#repeat_items
})
}

Expand Down