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

Support custom fields in the builder struct #245

Merged
merged 72 commits into from Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
f49ca28
ParsedLiteral: New utility type for parsing syn items from attrs
ijackson Mar 8, 2022
91f3ca2
BlockContents: impl FromMeta
ijackson Mar 14, 2022
2986ee4
Introduce BuilderFieldType enum
ijackson Mar 8, 2022
c934e3d
Break wrap_expression_in_some and BuilderFieldType::wrap_some
ijackson Mar 16, 2022
363c966
BuilderFieldType: Provide Precisely variant
ijackson Mar 16, 2022
1c18f6e
initializer: Swap two if arms
ijackson Mar 16, 2022
c4b0e55
initializer: Hoist builder_field variable, and move framing out of if
ijackson Mar 16, 2022
4983781
initializer: Allow specifying a custom conversion for initialisation
ijackson Mar 8, 2022
6b52753
Support builder(custom) attribute on fields
ijackson Mar 14, 2022
3375e27
Document (and doctest) builder(custom) field attribute
ijackson Mar 15, 2022
b7ab2ed
Make CustomConversion be an enum
ijackson Mar 14, 2022
5dcb274
builder(custom) defaults to simply moving value into target
ijackson Mar 16, 2022
b670413
Enhance documentation (and doctest) for custom fields
ijackson Mar 16, 2022
4904204
Custom fields: Further error checking and testing
ijackson Mar 24, 2022
dba50b7
Run rustfmt
ijackson Mar 25, 2022
531cadd
Use ? operator in FieldWithDefaults::custom_conversion
ijackson Apr 13, 2022
a7d545e
Make a comment more terse
ijackson Apr 13, 2022
81d946a
Reword error for custom= and default=
ijackson Apr 13, 2022
7303740
fixup! Reword error for custom= and default=
ijackson Apr 13, 2022
58a776f
fixup! Reword error for custom= and default=
ijackson Apr 13, 2022
713171e
fixup! Use ? operator in FieldWithDefaults::custom_conversion
ijackson Apr 13, 2022
a5d67d7
Move impl<'a> ToTokens for BuilderFieldType<'a>
ijackson Apr 13, 2022
7ddbbb5
Mark default_builder_field with cfg(test)
ijackson Apr 13, 2022
82c78e3
Report conflicting field options error only on builder(custom)
ijackson Apr 13, 2022
a2cf020
Rely on FromMeta impl for syn::Type in darling 0.13.3
ijackson Apr 13, 2022
13013b6
Revert "ParsedLiteral: New utility type for parsing syn items from at…
ijackson Apr 13, 2022
ccc2e0d
Rename BuilderFieldType::Precise from Precisely
ijackson Apr 13, 2022
beaec83
Replace BuilderField::field_enabled with BuilderFieldType::Phantom
ijackson Apr 13, 2022
fda1708
Setter: Move field type handling together
ijackson Apr 13, 2022
22fe6ca
Abolish BuilderFieldType::target_type in favour of setter_type_info
ijackson Apr 13, 2022
89ca85b
Correct comments for BuilderFieldType values
ijackson Apr 13, 2022
4c789d9
Prevent accidental loss of trailing comma by rearranging control flow
ijackson Apr 13, 2022
c6b9ec2
Run rustfmt
ijackson Apr 13, 2022
55d036c
Fix typo in comment
ijackson Apr 14, 2022
5a062bf
Change panic message
ijackson Apr 14, 2022
fed087a
Drop two unneeded #[darling(default)] annotations on Optional fields
ijackson Apr 14, 2022
2f6355f
Make wrap_expression_in_some not `pub`
ijackson Apr 14, 2022
c4d09bb
Move BuilderFieldType out from in the middle of BuilderField
ijackson Apr 14, 2022
75cfbfd
Promote custom() builder field contents to per-field arguments
ijackson Apr 14, 2022
5777b04
Replace Option<CustomConversion> with FieldConversion
ijackson Apr 14, 2022
848a12d
Move FieldConversion out from between Initializer and its impls
ijackson Apr 14, 2022
957532b
Adjust comment to explain why we have PhantomData for disabled fields
ijackson Apr 14, 2022
342acde
Adjust comment explaining privacy of PhantomData for disabled fields
ijackson Apr 14, 2022
f58d883
fixup! Move FieldConversion out from between Initializer and its impls
ijackson Apr 14, 2022
e24102e
Improve doc comment about `into`.
ijackson Apr 19, 2022
f7b57f8
Genericise wrap_expression_in_some
ijackson Apr 19, 2022
27d338c
Adjust comment about conflicting default and custom fields
ijackson Apr 19, 2022
ba5c462
fixup! Adjust comment about conflicting default and custom fields
ijackson Apr 19, 2022
c81b9a8
Move custom type and build_method into FieldMeta
ijackson Apr 19, 2022
02f1b73
Default vs builder spec conflict: Use span of "default"
ijackson Apr 19, 2022
0f1b0d6
Fix up semantic conflicts with #245
ijackson Apr 19, 2022
ba02e8b
Separate StructLevelFieldMeta from FieldLevelFieldMeta
ijackson Apr 19, 2022
2ef8ef2
fixup! Separate StructLevelFieldMeta from FieldLevelFieldMeta
ijackson Apr 19, 2022
21069d8
Honour field-level explicit visibility even for disabled Phantom
ijackson Apr 19, 2022
6e98460
Test setter(into) along with builder(field(type))
ijackson Apr 19, 2022
0605c9e
Fix compile fail test of defualt + field(build)
ijackson Apr 19, 2022
f8da6a0
Move trait impl to its proper place next to other impls on same type
ijackson Apr 19, 2022
8661dc4
Move test helper macro to next to tests
ijackson Apr 19, 2022
0f53cf7
Remove a pointless deref and re-ref
ijackson Apr 19, 2022
ad5403c
Fix comment for StructLevelFieldBuilder
ijackson Apr 19, 2022
b6ed16d
Fix doc comment for FieldLevelFieldMeta
ijackson Apr 19, 2022
30b2f99
Add reference to darling(flatten) issue
ijackson Apr 19, 2022
7027fc4
Use error accumulator in Field::resolve
ijackson Apr 19, 2022
f35f625
Fix a trailing newline.
ijackson Apr 19, 2022
4e5b0d5
Remove spuriously left-over commented-out line
ijackson Apr 19, 2022
9b32f3c
Fix build on MSRV 1.40, by replacing use of bool::then
ijackson Apr 19, 2022
1060b13
Fix formatting of MSRV fix
ijackson Apr 19, 2022
a397b3c
Fix error message for builder(default) + builder(field(default))
ijackson Apr 19, 2022
c421906
Fix typo in comment
ijackson Apr 19, 2022
147c8e1
Drop comment about wanting to use bool::then
ijackson Apr 19, 2022
95956fe
Change use of darling::Error::unsupported_format to custom
ijackson Apr 19, 2022
3bb2a34
Fix up test for changed error message
ijackson Apr 20, 2022
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
40 changes: 40 additions & 0 deletions derive_builder/src/lib.rs
Expand Up @@ -635,6 +635,46 @@
//! # }
//! ```
//!
//! # Completely custom fields in the builder
//!
//! Instead of having an `Option`, you can have whatever type you like:
//!
//! ```rust
//! # #[macro_use]
//! # extern crate derive_builder;
//! #[derive(Debug, PartialEq, Default, Builder, Clone)]
//! #[builder(derive(Debug, PartialEq))]
//! struct Lorem {
//! #[builder(setter(into), field(type = "u32"))]
//! ipsum: u32,
//!
//! #[builder(field(type = "String", build = "()"))]
//! dolor: (),
//!
//! #[builder(field(type = "&'static str", build = "self.amet.parse()?"))]
//! amet: u32,
//! }
//!
//! impl From<std::num::ParseIntError> for LoremBuilderError { // ...
//! # fn from(e: std::num::ParseIntError) -> LoremBuilderError {
//! # e.to_string().into()
//! # }
//! # }
//!
//! # fn main() {
//! let mut builder = LoremBuilder::default();
//! builder.ipsum(42u16).dolor("sit".into()).amet("12");
//! assert_eq!(builder, LoremBuilder { ipsum: 42, dolor: "sit".into(), amet: "12" });
//! let lorem = builder.build().unwrap();
//! assert_eq!(lorem, Lorem { ipsum: 42, dolor: (), amet: 12 });
//! # }
//! ```
//!
//! The builder field type (`type =`) must implement `Default`.
//!
//! The argument to `build` must be a literal string containing Rust code for the contents of a block, which must evaluate to the type of the target field.
//! It may refer to the builder struct as `self`, use `?`, etc.
//!
//! # **`#![no_std]`** Support (on Nightly)
//!
//! You can activate support for `#![no_std]` by adding `#[builder(no_std)]` to your struct
Expand Down
51 changes: 51 additions & 0 deletions derive_builder/tests/builder_field_custom.rs
@@ -0,0 +1,51 @@
#[macro_use]
extern crate pretty_assertions;
#[macro_use]
extern crate derive_builder;

use std::num::ParseIntError;

#[derive(Debug, PartialEq, Default, Builder, Clone)]
pub struct Lorem {
#[builder(field(type = "Option<usize>", build = "self.ipsum.unwrap_or(42) + 1"))]
ipsum: usize,

#[builder(field(type = "String", build = "self.dolor.parse()?"))]
dolor: u32,
TedDriggs marked this conversation as resolved.
Show resolved Hide resolved
}

impl From<ParseIntError> for LoremBuilderError {
fn from(e: ParseIntError) -> LoremBuilderError {
LoremBuilderError::ValidationError(e.to_string())
}
}

#[test]
fn custom_fields() {
let x = LoremBuilder::default().dolor("7".into()).build().unwrap();
ijackson marked this conversation as resolved.
Show resolved Hide resolved

assert_eq!(
x,
Lorem {
ipsum: 43,
dolor: 7,
}
);

let x = LoremBuilder::default()
.ipsum(Some(12))
.dolor("66".into())
.build()
.unwrap();

assert_eq!(
x,
Lorem {
ipsum: 13,
dolor: 66,
}
);

let x = LoremBuilder::default().build().unwrap_err().to_string();
assert_eq!(x, "cannot parse integer from empty string");
}
10 changes: 10 additions & 0 deletions derive_builder/tests/compile-fail/builder_field_custom.rs
@@ -0,0 +1,10 @@
#[macro_use]
extern crate derive_builder;

#[derive(Debug, PartialEq, Default, Builder, Clone)]
pub struct Lorem {
#[builder(default = "88", field(type = "usize", build = "self.ipsum.unwrap_or_else(42) + 1"))]
ijackson marked this conversation as resolved.
Show resolved Hide resolved
ipsum: usize,
}

fn main() {}
5 changes: 5 additions & 0 deletions derive_builder/tests/compile-fail/builder_field_custom.stderr
@@ -0,0 +1,5 @@
error: Unexpected meta-item format `#[builder(default)] and #[builder(build="...")] cannot be used together`
--> tests/compile-fail/builder_field_custom.rs:6:25
|
6 | #[builder(default = "88", field(type = "usize", build = "self.ipsum.unwrap_or_else(42) + 1"))]
| ^^^^
15 changes: 15 additions & 0 deletions derive_builder_core/src/block.rs
Expand Up @@ -44,6 +44,21 @@ impl From<syn::Expr> for BlockContents {
}
}

impl darling::FromMeta for BlockContents {
fn from_value(value: &syn::Lit) -> darling::Result<Self> {
if let syn::Lit::Str(s) = value {
let contents = BlockContents::try_from(s)?;
if contents.is_empty() {
Err(darling::Error::unknown_value("").with_span(s))
} else {
Ok(contents)
}
} else {
Err(darling::Error::unexpected_lit_type(value))
}
}
}

#[cfg(test)]
mod test {
use std::convert::TryInto;
Expand Down
97 changes: 66 additions & 31 deletions derive_builder_core/src/builder_field.rs
Expand Up @@ -33,45 +33,41 @@ use syn;
pub struct BuilderField<'a> {
/// Name of the target field.
pub field_ident: &'a syn::Ident,
/// Type of the target field.
///
/// The corresonding builder field will be `Option<field_type>`.
pub field_type: &'a syn::Type,
/// Whether the builder implements a setter for this field.
///
/// Note: We will fallback to `PhantomData` if the setter is disabled
/// to hack around issues with unused generic type parameters - at
/// least for now.
pub field_enabled: bool,
/// Type of the builder field.
pub field_type: BuilderFieldType<'a>,
/// Visibility of this builder field, e.g. `syn::Visibility::Public`.
pub field_visibility: Cow<'a, syn::Visibility>,
/// Attributes which will be attached to this builder field.
pub attrs: &'a [syn::Attribute],
}

impl<'a> ToTokens for BuilderField<'a> {
impl<'a> ToTokens for BuilderFieldType<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
if self.field_enabled {
let vis = &self.field_visibility;
let ident = self.field_ident;
let ty = self.field_type;
let attrs = self.attrs;

tokens.append_all(quote!(
#(#attrs)* #vis #ident: ::derive_builder::export::core::option::Option<#ty>,
));
} else {
let ident = self.field_ident;
let ty = self.field_type;
let attrs = self.attrs;

tokens.append_all(quote!(
#(#attrs)* #ident: ::derive_builder::export::core::marker::PhantomData<#ty>,
));
match self {
BuilderFieldType::Optional(ty) => tokens.append_all(quote!(
::derive_builder::export::core::option::Option<#ty>
)),
BuilderFieldType::Precise(ty) => ty.to_tokens(tokens),
BuilderFieldType::Phantom(ty) => tokens.append_all(quote!(
::derive_builder::export::core::marker::PhantomData<#ty>
)),
ijackson marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

impl<'a> ToTokens for BuilderField<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let ident = self.field_ident;
let vis = &self.field_visibility;
let ty = &self.field_type;
let attrs = self.attrs;
tokens.append_all(quote!(
#(#attrs)* #vis #ident: #ty,
));
// K let ty = self.field_type.target_type();
ijackson marked this conversation as resolved.
Show resolved Hide resolved
}
}

impl<'a> BuilderField<'a> {
/// Emits a struct field initializer that initializes the field to `Default::default`.
pub fn default_initializer_tokens(&self) -> TokenStream {
Expand All @@ -82,20 +78,55 @@ impl<'a> BuilderField<'a> {

/// Helper macro for unit tests. This is _only_ public in order to be accessible
/// from doc-tests too.
#[cfg(test)] // This contains a Box::leak, so is suitable only for tests
#[doc(hidden)]
#[macro_export]
macro_rules! default_builder_field {
ijackson marked this conversation as resolved.
Show resolved Hide resolved
() => {{
BuilderField {
field_ident: &syn::Ident::new("foo", ::proc_macro2::Span::call_site()),
field_type: &parse_quote!(String),
field_enabled: true,
field_type: BuilderFieldType::Optional(Box::leak(Box::new(parse_quote!(String)))),
TedDriggs marked this conversation as resolved.
Show resolved Hide resolved
field_visibility: ::std::borrow::Cow::Owned(parse_quote!(pub)),
attrs: &[parse_quote!(#[some_attr])],
}
}};
}

/// The type of a field in the builder struct
#[derive(Debug, Clone)]
pub enum BuilderFieldType<'a> {
TedDriggs marked this conversation as resolved.
Show resolved Hide resolved
ijackson marked this conversation as resolved.
Show resolved Hide resolved
/// The corresonding builder field will be `Option<field_type>`.
Optional(&'a syn::Type),
/// The corresponding builder field will be just this type
Precise(&'a syn::Type),
/// The corresponding builder field will be a PhantomData
///
/// We do this if if the field is disabled. We mustn't just completely omit the field from the builder:
/// if we did that, the builder might have unused generic parameters (since we copy the generics from
/// the target struct). Using a PhantomData of the original field type provides the right generic usage
/// (and the right variance). The alternative would be to give the user a way to separately control
/// the generics of the builder struct, which would be very awkward to use and complex to document.
/// We could just include the field anyway, as `Option<T>`, but this is wasteful of space, and it
/// seems good to explicitly suppress the existence of a variable that won't be set or read.
Phantom(&'a syn::Type),
}

impl<'a> BuilderFieldType<'a> {
/// Obtain type information for the builder field setter
///
/// Return value:
/// * `.0`: type of the argument to the setter function
/// (before application of `strip_option`, `into`)
/// * `.1`: whether the builder field is `Option<type>` rather than just `type`
pub fn setter_type_info(&'a self) -> (&'a syn::Type, bool) {
match self {
BuilderFieldType::Optional(ty) => (ty, true),
BuilderFieldType::Precise(ty) => (ty, false),
BuilderFieldType::Phantom(_ty) => panic!("phantom fields should never have setters"),
}
}
}

#[cfg(test)]
mod tests {
#[allow(unused_imports)]
Expand All @@ -117,7 +148,11 @@ mod tests {
#[test]
fn setter_disabled() {
let mut field = default_builder_field!();
field.field_enabled = false;
field.field_visibility = Cow::Owned(syn::Visibility::Inherited);
field.field_type = match field.field_type {
BuilderFieldType::Optional(ty) => BuilderFieldType::Phantom(ty),
_ => panic!(),
};

assert_eq!(
quote!(#field).to_string(),
Expand Down
13 changes: 1 addition & 12 deletions derive_builder_core/src/default_expression.rs
@@ -1,5 +1,3 @@
use std::convert::TryFrom;

use crate::BlockContents;
use quote::{ToTokens, TokenStreamExt};

Expand All @@ -23,16 +21,7 @@ impl darling::FromMeta for DefaultExpression {
}

fn from_value(value: &syn::Lit) -> darling::Result<Self> {
if let syn::Lit::Str(s) = value {
let contents = BlockContents::try_from(s)?;
if contents.is_empty() {
Err(darling::Error::unknown_value("").with_span(s))
} else {
Ok(Self::Explicit(contents))
}
} else {
Err(darling::Error::unexpected_lit_type(value))
}
Ok(Self::Explicit(BlockContents::from_value(value)?))
}
}

Expand Down