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 12 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
82 changes: 79 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,82 @@ struct Foo {
// Or you can set the default
#[builder(default = 20)]
z: i32,

// #[builder(default)] without parameter - don't require this field
// #[builder(setter(extend))] without parameter - start with the default and extend from there
#[builder(default, setter(extend(from_first, item_name = i0)))]
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(extend(from_first = |first| vec![first])))]
v1: Vec<i32>,

// Other `Extend` types are also supported.
#[builder(default, setter(extend))]
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).i0(4).v1_item(5).h_item((6, 7)).build()
== Foo {
x: 1,
y: Some(2),
z: 3,
v0: vec![4],
v1: vec![5],
h: extend![HashMap::new(); (6, 7)],
}
);

// 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_item((3, 4)).v1_item(5).i0(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_item(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)
.i0(2)
.i0(3)
.i0(4)
.v1_item(5)
.v1_item(6)
.h_item((7, 8))
.h_item((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
170 changes: 167 additions & 3 deletions src/field_info.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use proc_macro2::TokenStream;
use proc_macro2::{Span, TokenStream};
use quote::quote;
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::{expr_to_single_string, ident_to_type, path_to_single_ident, path_to_single_string, strip_raw_ident_prefix};

#[derive(Debug)]
pub struct FieldInfo<'a> {
Expand Down Expand Up @@ -89,9 +89,52 @@ pub struct SetterSettings {
pub doc: Option<syn::Expr>,
pub skip: bool,
pub auto_into: bool,
pub extend: Option<ExtendField>,
pub strip_option: bool,
}

#[derive(Debug, Clone)]
pub enum Configurable<T> {
Unset,
Auto { keyword_span: Span },
Custom { keyword_span: Span, value: T },
}

#[derive(Debug, Clone)]
pub struct Configured<T> {
pub keyword_span: Span,
pub value: T,
}

impl<T> Configurable<T> {
pub fn into_configured(self, auto: impl FnOnce(Span) -> T) -> Option<Configured<T>> {
match self {
Configurable::Unset => None,
Configurable::Auto { keyword_span } => Some(Configured {
keyword_span,
value: auto(keyword_span),
}),
Configurable::Custom { keyword_span, value } => Some(Configured { keyword_span, value }),
}
}

pub fn ensure_unset(&self, error_span: Span) -> Result<(), Error> {
if matches!(self, Configurable::Unset) {
Ok(())
} else {
Err(Error::new(error_span, "Duplicate option"))
}
}
}

#[derive(Debug, Clone)]
pub struct ExtendField {
pub keyword_span: Span,
pub from_first: Configurable<syn::ExprClosure>,
pub from_iter: Configurable<syn::ExprClosure>,
pub item_name: Option<syn::Ident>,
}

impl FieldBuilderAttr {
pub fn with(mut self, attrs: &[syn::Attribute]) -> Result<Self, Error> {
let mut skip_tokens = None;
Expand Down Expand Up @@ -234,6 +277,110 @@ 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() {
"extend" => {
if self.extend.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 extend = ExtendField {
keyword_span: name.span(),
from_first: Configurable::Unset,
from_iter: Configurable::Unset,
item_name: 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" => {
extend.from_first.ensure_unset(assign.left.span())?;
match *assign.right {
syn::Expr::Closure(closure) => {
extend.from_first = Configurable::Custom {
keyword_span: assign.left.span(),
value: closure,
}
}
other => {
return Err(Error::new_spanned(other, "Expected closure (|first| <...>)"))
}
}
}
"from_iter" => {
extend.from_iter.ensure_unset(assign.left.span())?;
match *assign.right {
syn::Expr::Closure(closure) => {
extend.from_iter = Configurable::Custom {
keyword_span: assign.left.span(),
value: closure,
}
}
other => {
return Err(Error::new_spanned(other, "Expected closure (|iter| <...>)"))
}
}
}
"item_name" => {
if extend.item_name.is_some() {
return Err(Error::new_spanned(assign.left, "Duplicate option"));
}
match *assign.right {
syn::Expr::Path(path) => {
let name = path_to_single_ident(&path.path)
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
extend.item_name = Some(name.clone())
}
other => return Err(Error::new_spanned(other, "Expected identifier")),
}
}
_ => {
return Err(Error::new_spanned(
&assign.left,
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"))?;
match name.as_str() {
"from_first" => {
extend.from_first.ensure_unset(path.span())?;
extend.from_first = Configurable::Auto {
keyword_span: path.span(),
};
}
"from_iter" => {
extend.from_iter.ensure_unset(path.span())?;
extend.from_iter = Configurable::Auto {
keyword_span: path.span(),
};
}
"item_name" => return Err(Error::new_spanned(path, "Expected (item_name = <...>)")),
_ => return Err(Error::new_spanned(path, format!("Unknown parameter {:?}", name))),
}
}
_ => return Err(Error::new_spanned(arg, "Expected (<...>) or (<...>=<...>)")),
}
}
self.extend = Some(extend);
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 @@ -249,6 +396,19 @@ impl SetterSettings {
}
}
)*
"extend" => {
if self.extend.is_some() {
Err(Error::new(path.span(), "Illegal setting - field is already calling extend(...) with the argument"))
} else {
self.extend = Some(ExtendField {
keyword_span: name.span(),
from_first: Configurable::Auto { keyword_span:name.span() },
from_iter: Configurable::Auto { keyword_span:name.span() },
item_name: None,
});
Ok(())
}
}
_ => Err(Error::new_spanned(
&path,
format!("Unknown setter parameter {:?}", name),
Expand Down Expand Up @@ -283,6 +443,10 @@ impl SetterSettings {
self.auto_into = false;
Ok(())
}
"extend" => {
self.extend = None;
Ok(())
}
"strip_option" => {
self.strip_option = false;
Ok(())
Expand All @@ -293,7 +457,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.

}
}
}
Loading