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

Nested builder support #253

Closed
wants to merge 9 commits into from
49 changes: 49 additions & 0 deletions derive_builder/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,52 @@ impl From<&'static str> for UninitializedFieldError {
Self::new(field_name)
}
}

/// Runtime error used when a sub-field's `build` method failed.
///
/// Represents an error from a sub-structure's builder, when
/// [`#[builder(sub_builder)]`](../index.html#sub-fields-with-builders-nested-builders)
/// is used.
/// Contains an `E`, the error type returned by the sub-structure's builder.
/// See
/// [Errors from the sub-structure builder](../index.html#errors-from-the-sub-structure-builder).
#[derive(Debug, Clone)]
pub struct SubfieldBuildError<E>(&'static str, E);

impl<E> SubfieldBuildError<E> {
/// Wrap an error in a `SubfieldBuildError`, attaching the specified field name.
pub fn new(field_name: &'static str, sub_builder_error: E) -> Self {
SubfieldBuildError(field_name, sub_builder_error)
}

/// Get the field name of the sub-field that couldn't be built.
pub fn field_name(&self) -> &'static str {
self.0
}

/// Get the error that was returned for the sub-field
pub fn sub_builder_error(&self) -> &E {
&self.1
}

/// Decompose the `SubfieldBuildError` into its constituent parts
pub fn into_parts(self) -> (&'static str, E) {
(self.0, self.1)
}
}

impl<E> fmt::Display for SubfieldBuildError<E>
where
E: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "in {}: {}", self.0, self.1)
}
}

#[cfg(feature = "std")]
// We do not implement `cause`.
// Rust EHWG recommend that we *either* provide a cause, *or* include the information in our
// own message. We really want to do the latter or users without a proper error reporter
// will get a very nugatory message.
impl<E> Error for SubfieldBuildError<E> where E: Error {}
86 changes: 84 additions & 2 deletions derive_builder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@
//!
//! ```rust
//! # extern crate derive_builder;
//! # use derive_builder::UninitializedFieldError;
//! # use derive_builder::{SubfieldBuildError, UninitializedFieldError};
//! # use std::fmt::{self, Display};
//! #
//! #[doc="Error type for LoremBuilder"]
Expand All @@ -606,6 +606,8 @@
//! }
//! impl From<UninitializedFieldError> for LoremBuilderError { // ...
//! # fn from(s: UninitializedFieldError) -> Self { todo!() } }
//! impl<E: std::error::Error> From<SubfieldBuildError<E>> for LoremBuilderError { // ...
//! # fn from(s: SubfieldBuildError<E>) -> Self { todo!() } }
//! impl Display for LoremBuilderError { // ...
//! # fn fmt(&self, _: &mut fmt::Formatter) -> fmt::Result { todo!() } }
//! impl std::error::Error for LoremBuilderError {}
Expand Down Expand Up @@ -635,6 +637,86 @@
//! # }
//! ```
//!
//! # Sub-fields with builders (nested builders)
//!
//! You can nest structs with builders.
//! The super-struct's builder will contain an instance of the sub-struct's builder
//! (not wrapped in `Option`).
//! With the mutable pattern (which is recommended), an accessor method will be generated. instead of a setter.
//!
//! The sub-structure's `build` method will be called by the super-structure's `build`.
//!
//! ```rust
//! // Definition
//!
//! # #[macro_use]
//! # extern crate derive_builder;
//! # use derive_builder::{UninitializedFieldError, SubfieldBuildError};
//! #[derive(Debug, PartialEq, Default, Builder, Clone)]
//! #[builder(build_fn(error="BuildError"))]
//! struct Lorem {
//! #[builder(sub_builder)]
//! ipsum: Ipsum,
//! }
//!
//! #[derive(Debug, PartialEq, Default, Builder, Clone)]
//! #[builder(build_fn(error="BuildError"))]
//! struct Ipsum {
//! i: usize,
//! }
//!
//! #[derive(Debug)]
//! struct BuildError { }
//! impl From<UninitializedFieldError> for BuildError {
//! # fn from(e: UninitializedFieldError) -> Self { BuildError { } } }
//! impl<E> From<SubfieldBuildError<E>> for BuildError {
//! # fn from(e: SubfieldBuildError<E>) -> Self { BuildError { } } }
//!
//! // Use
//!
//! # fn main() {
//! let mut lorem = LoremBuilder::default();
//! lorem.ipsum().i(42);
//! let lorem = lorem.build().unwrap();
//! assert_eq!(lorem,
//! Lorem { ipsum: Ipsum { i: 42 } });
//! # }
//! ```
//!
//! ## Errors from the sub-structure builder
//!
//! To allow reporting of which sub-structure failed to build,
//! the error `E` from the sub-field's builder will be wrapped up with the field name into a [`SubfieldBuildError<E>`].
//! That `SubfieldBuildError` must be convertible to the super-structure's builder error type (using `Into`).
//!
//! It can be helpful to specify a common error type for both the parent and sub-structures,
//! and manually implement conversions from `SubfieldBuildError` and `UninitializedFieldError`,
//! as shown in the example.
//! See also
//! [Error return type from autogenerated `build` function](#error-return-type-from-autogenerated-build-function).
//!
//! ## Adjusting the sub-structure builder name and build function
//!
//! ```rust
//! # #[macro_use]
//! # extern crate derive_builder;
//! #[derive(Debug, PartialEq, Default, Builder, Clone)]
//! struct Lorem {
//! #[builder(sub_builder(fn_name = "construct"), field(type = "IpsumConstructor"))]
//! ipsum: Ipsum,
//! }
//!
//! #[derive(Debug, PartialEq, Default, Builder, Clone)]
//! #[builder(name = "IpsumConstructor", build_fn(name = "construct"))]
//! struct Ipsum {
//! i: usize,
//! }
//! # fn main() { }
//! ```
//!
//! The default for the field level `builder(sub_builder(fn_name=...))`
//! is the parent's `builder(build_fn(name=...))`.
//!
//! # Completely custom fields in the builder
//!
//! Instead of having an `Option`, you can have whatever type you like:
Expand Down Expand Up @@ -722,7 +804,7 @@ mod error;
pub use derive_builder_macro::Builder;

#[doc(inline)]
pub use error::UninitializedFieldError;
pub use error::{SubfieldBuildError, UninitializedFieldError};

#[doc(hidden)]
pub mod export {
Expand Down
44 changes: 44 additions & 0 deletions derive_builder/tests/sub_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#[macro_use]
extern crate pretty_assertions;
#[macro_use]
extern crate derive_builder;

#[derive(Debug, PartialEq, Default, Builder, Clone)]
struct Lorem {
#[builder(sub_builder)]
ipsum: Ipsum,

#[builder(sub_builder(fn_name = "construct"), field(type = "DolorInput"))]
dolor: DolorTarget,
}

#[derive(Debug, PartialEq, Default, Builder, Clone)]
struct Ipsum {
i: usize,
}

#[derive(Debug, PartialEq, Default, Builder, Clone)]
#[builder(name = "DolorInput", build_fn(name = "construct"))]
struct DolorTarget {
d: String,
}

#[test]
fn builder_test() {
let mut x = LoremBuilder::default();
x.ipsum.i(42);
x.dolor.d(format!("dolor"));

let expected = Lorem {
ipsum: Ipsum { i: 42 },
dolor: DolorTarget { d: "dolor".into() },
};

assert_eq!(x.build().unwrap(), expected);

let x = LoremBuilder::default();
assert_eq!(
&x.build().unwrap_err().to_string(),
"in ipsum: `i` must be initialized"
);
}
12 changes: 12 additions & 0 deletions derive_builder_core/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,12 @@ impl<'a> ToTokens for Builder<'a> {
if self.std {
tokens.append_all(quote!(
impl std::error::Error for #builder_error_ident {}

impl<E: std::error::Error> ::derive_builder::export::core::convert::From<::derive_builder::SubfieldBuildError<E>> for #builder_error_ident {
fn from(e: ::derive_builder::SubfieldBuildError<E>) -> Self {
Self::ValidationError(::derive_builder::export::core::string::ToString::to_string(&e))
}
}
));
}
}
Expand Down Expand Up @@ -397,6 +403,12 @@ mod tests {
}

impl std::error::Error for FooBuilderError {}

impl<E: std::error::Error> ::derive_builder::export::core::convert::From<::derive_builder::SubfieldBuildError<E>> for FooBuilderError {
fn from(e: ::derive_builder::SubfieldBuildError<E>) -> Self {
Self::ValidationError(::derive_builder::export::core::string::ToString::to_string(&e))
}
}
));
}

Expand Down
14 changes: 14 additions & 0 deletions derive_builder_core/src/initializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ impl<'a> ToTokens for Initializer<'a> {
FieldConversion::Block(conv) => {
conv.to_tokens(tokens);
}
FieldConversion::Method(meth) => {
let span = self
.custom_error_type_span
.unwrap_or_else(|| self.field_ident.span());
let field_name = self.field_ident.to_string();
tokens.append_all(quote_spanned!(span=>
::derive_builder::export::core::result::Result::map_err(
self.#builder_field.#meth(),
|e| ::derive_builder::SubfieldBuildError::new(#field_name, e),
)?
));
}
FieldConversion::Move => tokens.append_all(quote!( self.#builder_field )),
FieldConversion::OptionOrDefault => {
let match_some = self.match_some();
Expand Down Expand Up @@ -148,6 +160,8 @@ pub enum FieldConversion<'a> {
OptionOrDefault,
/// Custom conversion is a block contents expression
Block(&'a BlockContents),
/// Custom conversion is a method to be called on the corresponding builder field
Method(&'a syn::Ident),
/// Custom conversion is just to move the field from the builder
Move,
}
Expand Down
71 changes: 67 additions & 4 deletions derive_builder_core/src/macro_options/darling_opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ pub struct Field {
try_setter: Flag,
#[darling(default)]
field: FieldLevelFieldMeta,
/// Builder field is itself a nested builder.
sub_builder: Option<darling::util::Override<FieldSubBuilder>>,
#[darling(skip)]
field_attrs: Vec<Attribute>,
#[darling(skip)]
Expand Down Expand Up @@ -361,6 +363,8 @@ impl Field {
);
};

self.resolve_sub_builder_field_type(&mut errors);

errors.handle(distribute_and_unnest_attrs(
&mut self.attrs,
&mut [
Expand All @@ -371,6 +375,36 @@ impl Field {

errors.finish_with(self)
}

/// Check `self.sub_builder` to set `self.field.builder_type`
///
/// Defaults to trying to add `Builder` to the last path component in the field type
fn resolve_sub_builder_field_type(&mut self, errors: &mut darling::error::Accumulator) {
if !(self.sub_builder.is_some() && self.field.builder_type.is_none()) {
return;
}

self.field.builder_type = (|| {
let ty = &self.ty;
let mut out = ty.clone();
let p = match &mut out {
syn::Type::Path(p) => p,
_ => return None,
};
let last_ident = &mut p.path.segments.last_mut()?.ident;
*last_ident = format_ident!("{}Builder", last_ident);
Some(out)
})()
.or_else(|| {
errors.push(
darling::Error::custom(
"field type is not a non-empty path, sub-builder type must be specified",
)
.with_span(&self.ty),
);
None
});
}
}

/// Divide a list of attributes into multiple partially-overlapping output lists.
Expand Down Expand Up @@ -481,6 +515,17 @@ impl Visibility for Field {
}
}

/// Options for the field-level `sub_builder` property
///
/// Presence of this option implies a different default for the builder field type.
#[derive(Debug, Clone, Default, FromMeta)]
#[darling(default)]
struct FieldSubBuilder {
/// defaults to the same name as on the super-struct
#[darling(default)]
fn_name: Option<Ident>,
}

fn default_create_empty() -> Ident {
Ident::new("create_empty", Span::call_site())
}
Expand Down Expand Up @@ -837,10 +882,19 @@ impl<'a> FieldWithDefaults<'a> {
}

pub fn conversion(&'a self) -> FieldConversion<'a> {
match (&self.field.field.builder_type, &self.field.field.build) {
(_, Some(block)) => FieldConversion::Block(block),
(Some(_), None) => FieldConversion::Move,
(None, None) => FieldConversion::OptionOrDefault,
if let Some(block) = &self.field.field.build {
FieldConversion::Block(block)
} else if let Some(sub) = &self.field.sub_builder {
let method = sub
.as_ref()
.explicit()
.and_then(|sub| sub.fn_name.as_ref())
.unwrap_or(&self.parent.build_fn.name);
FieldConversion::Method(method)
} else if self.field.field.builder_type.is_some() {
FieldConversion::Move
} else {
FieldConversion::OptionOrDefault
}
}

Expand All @@ -855,6 +909,14 @@ impl<'a> FieldWithDefaults<'a> {
pub fn deprecation_notes(&self) -> &DeprecationNotes {
&self.parent.deprecation_notes
}

pub fn sub_accessor(&self) -> bool {
match self.pattern() {
BuilderPattern::Mutable => self.field.sub_builder.is_some(),
BuilderPattern::Owned => false,
BuilderPattern::Immutable => false,
}
}
}

/// Converters to codegen structs
Expand All @@ -874,6 +936,7 @@ impl<'a> FieldWithDefaults<'a> {
strip_option: self.setter_strip_option(),
deprecation_notes: self.deprecation_notes(),
each: self.field.setter.each.as_ref(),
sub_accessor: self.sub_accessor(),
}
}

Expand Down