-
Notifications
You must be signed in to change notification settings - Fork 52
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
base: master
Are you sure you want to change the base?
Changes from 6 commits
486480b
c824836
03e0e58
b19a01b
4376566
8e4b301
7635462
a3d3e31
c9bba54
586d3dc
23ca53a
094a827
4a2afb0
a3006a1
9bb9dbf
4c67cf4
c8abc4d
8cc816a
3a8ff5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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: | ||
|
@@ -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])))] | ||
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)], | ||
} | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
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; | ||
|
@@ -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; | ||
|
@@ -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 { | ||
|
@@ -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), | ||
|
@@ -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(()) | ||
|
@@ -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 (<...>(<...>))")), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
} | ||
} | ||
} |
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}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tend to use My earlier hack with |
||
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, | ||
|
@@ -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 | ||
|
@@ -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 ),* > | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 The generic type parameter |
||
// 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)] | ||
|
@@ -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 | ||
}) | ||
} | ||
|
||
|
There was a problem hiding this comment.
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 thaninitializer
here, and will nicely pair withfrom_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
andfrom_iter
without assignment to generate the default each. This would mirror howdefault
works asbuilder(…)
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 🤔
There was a problem hiding this comment.
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 keepI: Iterator
, and each time we extend with the builder, we can make anstd::iter::Chain
. We can still specifyfrom_iter = |it| ...
ifFromIterator
is not available for that collection or if for whatever reason we need custom behavior.