Skip to content

Commit

Permalink
Allow opting out of impl Default (#228)
Browse files Browse the repository at this point in the history
* Allow opting out of impl Default
* Add create_empty inherent method

Fixes #227
  • Loading branch information
TedDriggs committed Mar 8, 2022
1 parent a560de8 commit cb8e497
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 15 deletions.
74 changes: 74 additions & 0 deletions derive_builder/examples/custom_constructor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#![allow(dead_code)]

#[macro_use]
extern crate derive_builder;

#[derive(Debug, Clone)]
pub enum ContentType {
Json,
Xml,
}

impl Default for ContentType {
fn default() -> Self {
Self::Json
}
}

#[derive(Debug, Builder)]
#[builder(
custom_constructor,
create_empty = "empty",
build_fn(private, name = "fallible_build")
)]
pub struct ApiClient {
// To make sure `host` and `key` are not changed after creation, tell derive_builder
// to create the fields but not to generate setters.
#[builder(setter(custom))]
host: String,
#[builder(setter(custom))]
key: String,
// uncommenting this field will cause the unit-test below to fail
// baz: String,
#[builder(default)]
content_type: ContentType,
}

impl ApiClient {
pub fn new(host: impl Into<String>, key: impl Into<String>) -> ApiClientBuilder {
ApiClientBuilder {
host: Some(host.into()),
key: Some(key.into()),
..ApiClientBuilder::empty()
}
}
}

impl ApiClientBuilder {
pub fn build(&self) -> ApiClient {
self.fallible_build()
.expect("All required fields were initialized")
}
}

fn main() {
dbg!(ApiClient::new("hello", "world")
.content_type(ContentType::Xml)
.build());
}

#[cfg(test)]
mod tests {
use super::ApiClient;

/// If any required fields are added to ApiClient and ApiClient::new was not updated,
/// the field will be `None` when this "infallible" build method is called, resulting
/// in a panic. Panicking is appropriate here, since the author of the builder promised
/// the caller that it was safe to call new(...) followed immediately by build(), and
/// the builder author is also the person who can correct the coding error by setting
/// a default for the new property or changing the signature of `new`
#[test]
fn api_client_new_collects_all_required_fields() {
ApiClient::new("hello", "world").build();
}
}
52 changes: 52 additions & 0 deletions derive_builder/tests/custom_constructor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#[macro_use]
extern crate pretty_assertions;
#[macro_use]
extern crate derive_builder;

#[derive(Debug, PartialEq, Eq, Builder)]
#[builder(custom_constructor, build_fn(private, name = "fallible_build"))]
struct Request {
url: &'static str,
username: &'static str,
#[builder(default, setter(into))]
password: Option<&'static str>,
}

impl RequestBuilder {
pub fn new(url: &'static str, username: &'static str) -> Self {
Self {
url: Some(url),
username: Some(username),
..Self::create_empty()
}
}

pub fn build(&self) -> Request {
self.fallible_build()
.expect("All required fields set upfront")
}
}

#[test]
fn new_then_build_succeeds() {
assert_eq!(
RequestBuilder::new("...", "!!!").build(),
Request {
url: "...",
username: "!!!",
password: None
}
);
}

#[test]
fn new_then_set_succeeds() {
assert_eq!(
RequestBuilder::new("...", "!!!").password("test").build(),
Request {
url: "...",
username: "!!!",
password: Some("test")
}
);
}
131 changes: 116 additions & 15 deletions derive_builder_core/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ pub struct Builder<'a> {
pub pattern: BuilderPattern,
/// Traits to automatically derive on the builder type.
pub derives: &'a [Path],
/// When true, generate `impl Default for #ident` which calls the `create_empty` inherent method.
///
/// Note that the name of `create_empty` can be overridden; see the `create_empty` field for more.
pub impl_default: bool,
/// The identifier of the inherent method that creates a builder with all fields set to
/// `None` or `PhantomData`.
///
/// This method will be invoked by `impl Default` for the builder, but it is also accessible
/// to `impl` blocks on the builder that expose custom constructors.
pub create_empty: syn::Ident,
/// Type parameters and lifetimes attached to this builder's struct
/// definition.
pub generics: Option<&'a syn::Generics>,
Expand Down Expand Up @@ -157,6 +167,7 @@ impl<'a> ToTokens for Builder<'a> {
.unwrap_or((None, None, None));
let builder_fields = &self.fields;
let builder_field_initializers = &self.field_initializers;
let create_empty = &self.create_empty;
let functions = &self.functions;

// Create the comma-separated set of derived traits for the builder
Expand Down Expand Up @@ -198,17 +209,26 @@ impl<'a> ToTokens for Builder<'a> {
impl #impl_generics #builder_ident #ty_generics #where_clause {
#(#functions)*
#deprecation_notes
}

impl #impl_generics ::derive_builder::export::core::default::Default for #builder_ident #ty_generics #where_clause {
fn default() -> Self {
/// Create an empty builder, with all fields set to `None` or `PhantomData`.
fn #create_empty() -> Self {
Self {
#(#builder_field_initializers)*
}
}
}
));

if self.impl_default {
tokens.append_all(quote!(
impl #impl_generics ::derive_builder::export::core::default::Default for #builder_ident #ty_generics #where_clause {
fn default() -> Self {
Self::#create_empty()
}
}
));
}

if self.generate_error {
let builder_error_ident = format_ident!("{}Error", builder_ident);
let builder_error_doc = format!("Error type for {}", builder_ident);
Expand Down Expand Up @@ -323,6 +343,8 @@ macro_rules! default_builder {
ident: syn::Ident::new("FooBuilder", ::proc_macro2::Span::call_site()),
pattern: Default::default(),
derives: &vec![],
impl_default: true,
create_empty: syn::Ident::new("create_empty", ::proc_macro2::Span::call_site()),
generics: None,
visibility: parse_quote!(pub),
fields: vec![quote!(foo: u32,)],
Expand All @@ -342,6 +364,7 @@ mod tests {
#[allow(unused_imports)]
use super::*;
use proc_macro2::TokenStream;
use syn::Ident;

fn add_generated_error(result: &mut TokenStream) {
result.append_all(quote!(
Expand Down Expand Up @@ -408,15 +431,73 @@ mod tests {
fn bar () -> {
unimplemented!()
}

/// Create an empty builder, with all fields set to `None` or `PhantomData`.
fn create_empty() -> Self {
Self {
foo: ::derive_builder::export::core::default::Default::default(),
}
}
}

impl ::derive_builder::export::core::default::Default for FooBuilder {
fn default() -> Self {
Self::create_empty()
}
}
));

add_generated_error(&mut result);

result
}
.to_string()
);
}

#[test]
fn rename_create_empty() {
let mut builder = default_builder!();
builder.create_empty = Ident::new("empty", proc_macro2::Span::call_site());

assert_eq!(
quote!(#builder).to_string(),
{
let mut result = quote!();

#[cfg(not(feature = "clippy"))]
result.append_all(quote!(#[allow(clippy::all)]));

result.append_all(quote!(
#[derive(Clone)]
pub struct FooBuilder {
foo: u32,
}
));

#[cfg(not(feature = "clippy"))]
result.append_all(quote!(#[allow(clippy::all)]));

result.append_all(quote!(
#[allow(dead_code)]
impl FooBuilder {
fn bar () -> {
unimplemented!()
}

/// Create an empty builder, with all fields set to `None` or `PhantomData`.
fn empty() -> Self {
Self {
foo: ::derive_builder::export::core::default::Default::default(),
}
}
}

impl ::derive_builder::export::core::default::Default for FooBuilder {
fn default() -> Self {
Self::empty()
}
}
));

add_generated_error(&mut result);
Expand Down Expand Up @@ -463,15 +544,20 @@ mod tests {
fn bar() -> {
unimplemented!()
}
}

impl<'a, T: Debug + ::derive_builder::export::core::clone::Clone> ::derive_builder::export::core::default::Default for FooBuilder<'a, T> where T: PartialEq {
fn default() -> Self {
/// Create an empty builder, with all fields set to `None` or `PhantomData`.
fn create_empty() -> Self {
Self {
foo: ::derive_builder::export::core::default::Default::default(),
}
}
}

impl<'a, T: Debug + ::derive_builder::export::core::clone::Clone> ::derive_builder::export::core::default::Default for FooBuilder<'a, T> where T: PartialEq {
fn default() -> Self {
Self::create_empty()
}
}
));

add_generated_error(&mut result);
Expand Down Expand Up @@ -521,13 +607,18 @@ mod tests {
fn bar() -> {
unimplemented!()
}

/// Create an empty builder, with all fields set to `None` or `PhantomData`.
fn create_empty() -> Self {
Self {
foo: ::derive_builder::export::core::default::Default::default(),
}
}
}

impl<'a, T: 'a + Default + ::derive_builder::export::core::clone::Clone> ::derive_builder::export::core::default::Default for FooBuilder<'a, T> where T: PartialEq {
fn default() -> Self {
Self {
foo: ::derive_builder::export::core::default::Default::default(),
}
Self::create_empty()
}
}
));
Expand Down Expand Up @@ -576,14 +667,19 @@ mod tests {
fn bar() -> {
unimplemented!()
}

/// Create an empty builder, with all fields set to `None` or `PhantomData`.
fn create_empty() -> Self {
Self {
foo: ::derive_builder::export::core::default::Default::default(),
}
}
}

impl<'a, T: Debug> ::derive_builder::export::core::default::Default for FooBuilder<'a, T>
where T: PartialEq {
fn default() -> Self {
Self {
foo: ::derive_builder::export::core::default::Default::default(),
}
Self::create_empty()
}
}
));
Expand Down Expand Up @@ -633,15 +729,20 @@ mod tests {
fn bar () -> {
unimplemented!()
}
}

impl ::derive_builder::export::core::default::Default for FooBuilder {
fn default() -> Self {
/// Create an empty builder, with all fields set to `None` or `PhantomData`.
fn create_empty() -> Self {
Self {
foo: ::derive_builder::export::core::default::Default::default(),
}
}
}

impl ::derive_builder::export::core::default::Default for FooBuilder {
fn default() -> Self {
Self::create_empty()
}
}
));

add_generated_error(&mut result);
Expand Down
Loading

0 comments on commit cb8e497

Please sign in to comment.