diff --git a/derive_builder/examples/custom_error.rs b/derive_builder/examples/custom_error.rs new file mode 100644 index 00000000..766e30ba --- /dev/null +++ b/derive_builder/examples/custom_error.rs @@ -0,0 +1,54 @@ +//! This example shows using custom validation with a non-string error type. +//! +//! This relies on how the generated build function is constructed; the validator +//! is invoked in conjunction with the `?` operator, so anything that converts to +//! the generated `FooBuilderError` type is valid. + +#[macro_use] +extern crate derive_builder; + +use derive_builder::UninitializedFieldError; +use std::fmt; + +fn validate_age(builder: &ExampleBuilder) -> Result<(), Error> { + match builder.age { + Some(age) if age > 150 => Err(Error::UnrealisticAge(age)), + _ => Ok(()), + } +} + +#[derive(Debug, Builder)] +#[builder(setter(into), build_fn(validate = "validate_age", error = "Error"))] +struct Example { + name: String, + age: usize, +} + +enum Error { + UninitializedField(&'static str), + UnrealisticAge(usize), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::UnrealisticAge(age) => write!(f, "Nobody is {} years old", age), + Self::UninitializedField(field) => write!(f, "Required field '{}' not set", field), + } + } +} + +impl From for Error { + fn from(e: UninitializedFieldError) -> Self { + Self::UninitializedField(e.field_name()) + } +} + +fn main() { + let person_err = ExampleBuilder::default() + .name("Jane Doe") + .age(200usize) + .build() + .unwrap_err(); + println!("{}", person_err); +} diff --git a/derive_builder/examples/custom_error_generic.rs b/derive_builder/examples/custom_error_generic.rs new file mode 100644 index 00000000..7795ffb3 --- /dev/null +++ b/derive_builder/examples/custom_error_generic.rs @@ -0,0 +1,55 @@ +//! This example shows combining generics with custom errors and validation. +//! +//! Note the use of the type parameter in the `#[builder(...)]` attribute. + +#[macro_use] +extern crate derive_builder; + +use derive_builder::UninitializedFieldError; + +trait Popular { + fn is_popular(&self) -> bool; +} + +impl<'a> Popular for &'a str { + fn is_popular(&self) -> bool { + !self.starts_with('b') + } +} + +#[derive(Debug, Builder)] +#[builder(build_fn(validate = "check_person", error = "Error"))] +struct Person { + name: N, + age: u16, +} + +#[derive(Debug)] +enum Error { + UninitializedField(&'static str), + UnpopularName(N), +} + +impl From for Error { + fn from(error: UninitializedFieldError) -> Self { + Self::UninitializedField(error.field_name()) + } +} + +fn check_person(builder: &PersonBuilder) -> Result<(), Error> { + if let Some(name) = &builder.name { + if !name.is_popular() { + return Err(Error::UnpopularName(name.clone())); + } + } + + Ok(()) +} + +fn main() { + dbg!(PersonBuilder::default() + .name("bill") + .age(71) + .build() + .unwrap_err()); +} diff --git a/derive_builder/src/lib.rs b/derive_builder/src/lib.rs index 787abc49..ab48ecb2 100644 --- a/derive_builder/src/lib.rs +++ b/derive_builder/src/lib.rs @@ -541,10 +541,14 @@ #![deny(warnings)] #![cfg_attr(not(feature = "std"), no_std)] +extern crate derive_builder_core; extern crate derive_builder_macro; pub use derive_builder_macro::Builder; +#[doc(inline)] +pub use derive_builder_core::UninitializedFieldError; + #[doc(hidden)] pub mod export { pub mod core { diff --git a/derive_builder/tests/compile-fail/custom_error_generic_missing_bound.rs b/derive_builder/tests/compile-fail/custom_error_generic_missing_bound.rs new file mode 100644 index 00000000..b9b78dea --- /dev/null +++ b/derive_builder/tests/compile-fail/custom_error_generic_missing_bound.rs @@ -0,0 +1,44 @@ +#[macro_use] +extern crate derive_builder; + +use derive_builder::UninitializedFieldError; + +trait Popular { + fn is_popular(&self) -> bool; +} + +impl<'a> Popular for &'a str { + fn is_popular(&self) -> bool { + !self.starts_with('b') + } +} + +#[derive(Debug, Builder)] +#[builder(build_fn(validate = "check_person", error = "Error"))] +struct Person { + name: N, + age: u16, +} + +enum Error { + UninitializedField(&'static str), + UnpopularName(N), +} + +impl From for Error { + fn from(error: UninitializedFieldError) -> Self { + Self::UninitializedField(error.field_name()) + } +} + +fn check_person(builder: &PersonBuilder) -> Result<(), Error> { + if let Some(name) = &builder.name { + if !name.is_popular() { + return Err(Error::UnpopularName(name.clone())); + } + } + + Ok(()) +} + +fn main() {} diff --git a/derive_builder/tests/compile-fail/custom_error_generic_missing_bound.stderr b/derive_builder/tests/compile-fail/custom_error_generic_missing_bound.stderr new file mode 100644 index 00000000..2b8e2b29 --- /dev/null +++ b/derive_builder/tests/compile-fail/custom_error_generic_missing_bound.stderr @@ -0,0 +1,10 @@ +error[E0277]: the trait bound `N: Popular` is not satisfied + --> $DIR/custom_error_generic_missing_bound.rs:17:31 + | +17 | #[builder(build_fn(validate = "check_person", error = "Error"))] + | ^^^^^^^^^^^^^^ the trait `Popular` is not implemented for `N` +18 | struct Person { + | - consider adding a `where N: Popular` bound +... +34 | fn check_person(builder: &PersonBuilder) -> Result<(), Error> { + | ------------ ------- required by this bound in `check_person` diff --git a/derive_builder/tests/compile-fail/custom_error_no_from.rs b/derive_builder/tests/compile-fail/custom_error_no_from.rs new file mode 100644 index 00000000..f052a173 --- /dev/null +++ b/derive_builder/tests/compile-fail/custom_error_no_from.rs @@ -0,0 +1,37 @@ +#[macro_use] +extern crate derive_builder; + +fn validate_age(age: usize) -> Result<(), Error> { + if age > 200 { + Err(Error::UnrealisticAge(age)) + } else { + Ok(()) + } +} + +fn check_person(builder: &PersonBuilder) -> Result<(), Error> { + if let Some(age) = builder.age { + validate_age(age) + } else { + Ok(()) + } +} + +#[derive(Builder)] +#[builder(build_fn(validate = "check_person", error = "Error"))] +struct Person { + name: String, + age: usize, +} + +// NOTE: This enum has a variant for the uninitialized field case (called MissingData) +// but has forgotten `impl From`, which is a +// compile-blocking mistake. +#[derive(Debug)] +enum Error { + /// A required field is not filled out. + MissingData(&'static str), + UnrealisticAge(usize), +} + +fn main() {} diff --git a/derive_builder/tests/compile-fail/custom_error_no_from.stderr b/derive_builder/tests/compile-fail/custom_error_no_from.stderr new file mode 100644 index 00000000..7b25c2c2 --- /dev/null +++ b/derive_builder/tests/compile-fail/custom_error_no_from.stderr @@ -0,0 +1,8 @@ +error[E0277]: the trait bound `Error: std::convert::From` is not satisfied + --> $DIR/custom_error_no_from.rs:21:55 + | +21 | #[builder(build_fn(validate = "check_person", error = "Error"))] + | ^^^^^^^ the trait `std::convert::From` is not implemented for `Error` + | + = note: required because of the requirements on the impl of `std::convert::Into` for `derive_builder::UninitializedFieldError` + = note: required by `std::convert::Into::into` diff --git a/derive_builder/tests/custom_default.rs b/derive_builder/tests/custom_default.rs index b2dfbf66..8fd6d215 100644 --- a/derive_builder/tests/custom_default.rs +++ b/derive_builder/tests/custom_default.rs @@ -4,6 +4,7 @@ extern crate pretty_assertions; extern crate derive_builder; mod field_level { + use derive_builder::UninitializedFieldError; #[derive(Debug, PartialEq, Default, Builder, Clone)] struct Lorem { required: String, @@ -16,7 +17,7 @@ mod field_level { #[builder(default = r#"format!("{}-{}-{}-{}", Clone::clone(self.required .as_ref() - .ok_or("required must be initialized")?), + .ok_or_else(|| UninitializedFieldError::new("required"))?), match self.explicit_default { Some(ref x) => x, None => "EMPTY" }, self.escaped_default.as_ref().map(|x| x.as_ref()).unwrap_or("EMPTY"), if let Some(ref x) = self.raw_default { x } else { "EMPTY" })"#)] diff --git a/derive_builder/tests/run-pass/custom_error_default.rs b/derive_builder/tests/run-pass/custom_error_default.rs new file mode 100644 index 00000000..da0d8f62 --- /dev/null +++ b/derive_builder/tests/run-pass/custom_error_default.rs @@ -0,0 +1,49 @@ +//! This test ensures custom errors don't need a conversion from `UninitializedFieldError` +//! if uninitialized fields are impossible. + +#[macro_use] +extern crate derive_builder; + +#[derive(Default, Builder)] +#[builder(default, build_fn(validate = "check_person", error = "Error"))] +struct Person { + name: String, + age: u16, +} + +/// An error that deliberately doesn't have `impl From`; as long +/// as `PersonBuilder` uses `Person::default` then missing field errors are never possible. +enum Error { + UnpopularName(String), + UnrealisticAge(u16), +} + +fn check_age_realistic(age: u16) -> Result<(), Error> { + if age > 150 { + Err(Error::UnrealisticAge(age)) + } else { + Ok(()) + } +} + +fn check_name_popular(name: &str) -> Result<(), Error> { + if name.starts_with('B') { + Err(Error::UnpopularName(name.to_string())) + } else { + Ok(()) + } +} + +fn check_person(builder: &PersonBuilder) -> Result<(), Error> { + if let Some(age) = &builder.age { + check_age_realistic(*age)?; + } + + if let Some(name) = &builder.name { + check_name_popular(name)?; + } + + Ok(()) +} + +fn main() {} diff --git a/derive_builder_core/src/build_method.rs b/derive_builder_core/src/build_method.rs index 26d7c4fe..f3603892 100644 --- a/derive_builder_core/src/build_method.rs +++ b/derive_builder_core/src/build_method.rs @@ -2,6 +2,7 @@ use doc_comment_from; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, TokenStreamExt}; use syn; +use syn::spanned::Spanned; use Block; use BuilderPattern; use Initializer; @@ -51,7 +52,7 @@ pub struct BuildMethod<'a> { /// Type parameters and lifetimes attached to this builder struct. pub target_ty_generics: Option>, /// Type of error. - pub error_ty: syn::Ident, + pub error_ty: syn::Path, /// Field initializers for the target type. pub initializers: Vec, /// Doc-comment of the builder struct. @@ -81,7 +82,10 @@ impl<'a> ToTokens for BuildMethod<'a> { let ident = syn::Ident::new(DEFAULT_STRUCT_NAME, Span::call_site()); quote!(let #ident: #target_ty #target_ty_generics = #default_expr;) }); - let validate_fn = self.validate_fn.as_ref().map(|vfn| quote!(#vfn(&self)?;)); + let validate_fn = self + .validate_fn + .as_ref() + .map(|vfn| quote_spanned!(vfn.span() => #vfn(&self)?;)); let error_ty = &self.error_ty; if self.enabled { @@ -119,6 +123,11 @@ impl<'a> BuildMethod<'a> { } } +// pub struct BuildMethodError { +// is_generated: bool, +// ident: syn::Ident, +// } + /// Helper macro for unit tests. This is _only_ public in order to be accessible /// from doc-tests too. #[doc(hidden)] @@ -132,7 +141,7 @@ macro_rules! default_build_method { pattern: BuilderPattern::Mutable, target_ty: &syn::Ident::new("Foo", ::proc_macro2::Span::call_site()), target_ty_generics: None, - error_ty: syn::Ident::new("FooBuilderError", ::proc_macro2::Span::call_site()), + error_ty: syn::parse_quote!(FooBuilderError), initializers: vec![quote!(foo: self.foo,)], doc_comment: None, default_struct: None, diff --git a/derive_builder_core/src/builder.rs b/derive_builder_core/src/builder.rs index ef1b3aea..cbabb946 100644 --- a/derive_builder_core/src/builder.rs +++ b/derive_builder_core/src/builder.rs @@ -126,6 +126,10 @@ pub struct Builder<'a> { pub field_initializers: Vec, /// Functions of the builder struct, e.g. `fn bar() -> { unimplemented!() }` pub functions: Vec, + /// Whether or not a generated error type is required. + /// + /// This would be `false` in the case where an already-existing error is to be used. + pub generate_error: bool, /// Whether this builder must derive `Clone`. /// /// This is true even for a builder using the `owned` pattern if there is a field whose setter @@ -176,49 +180,12 @@ impl<'a> ToTokens for Builder<'a> { #[cfg(not(feature = "clippy"))] tokens.append_all(quote!(#[allow(clippy::all)])); - let builder_error_ident = format_ident!("{}Error", builder_ident); - let builder_error_doc = format!("Error type for {}", builder_ident); - tokens.append_all(quote!( #derive_attr #builder_doc_comment #builder_vis struct #builder_ident #struct_generics #where_clause { #(#builder_fields)* } - - #[doc=#builder_error_doc] - #[derive(Debug)] - #[non_exhaustive] - #builder_vis enum #builder_error_ident { - /// Uninitialized field - UninitializedField(&'static str), - /// Custom validation error - ValidationError(String), - } - - impl ::derive_builder::export::core::convert::From<&'static str> for #builder_error_ident { - fn from(s: &'static str) -> Self { - Self::UninitializedField(s) - } - } - - impl ::derive_builder::export::core::convert::From for #builder_error_ident { - fn from(s: String) -> Self { - Self::ValidationError(s) - } - } - - impl ::derive_builder::export::core::fmt::Display for #builder_error_ident { - fn fmt(&self, f: &mut ::derive_builder::export::core::fmt::Formatter) -> ::derive_builder::export::core::fmt::Result { - match self { - Self::UninitializedField(ref field) => write!(f, "`{}` must be initialized", field), - Self::ValidationError(ref error) => write!(f, "{}", error), - } - } - } - - #[cfg(not(no_std))] - impl std::error::Error for #builder_error_ident {} )); #[cfg(not(feature = "clippy"))] @@ -239,6 +206,47 @@ impl<'a> ToTokens for Builder<'a> { } } )); + + if self.generate_error { + let builder_error_ident = format_ident!("{}Error", builder_ident); + let builder_error_doc = format!("Error type for {}", builder_ident); + + tokens.append_all(quote!( + #[doc=#builder_error_doc] + #[derive(Debug)] + #[non_exhaustive] + #builder_vis enum #builder_error_ident { + /// Uninitialized field + UninitializedField(&'static str), + /// Custom validation error + ValidationError(String), + } + + impl ::derive_builder::export::core::convert::From<::derive_builder::UninitializedFieldError> for #builder_error_ident { + fn from(s: ::derive_builder::UninitializedFieldError) -> Self { + Self::UninitializedField(s.field_name()) + } + } + + impl ::derive_builder::export::core::convert::From for #builder_error_ident { + fn from(s: String) -> Self { + Self::ValidationError(s) + } + } + + impl ::derive_builder::export::core::fmt::Display for #builder_error_ident { + fn fmt(&self, f: &mut ::derive_builder::export::core::fmt::Formatter) -> ::derive_builder::export::core::fmt::Result { + match self { + Self::UninitializedField(ref field) => write!(f, "`{}` must be initialized", field), + Self::ValidationError(ref error) => write!(f, "{}", error), + } + } + } + + #[cfg(not(no_std))] + impl std::error::Error for #builder_error_ident {} + )); + } } } } @@ -315,6 +323,7 @@ macro_rules! default_builder { fields: vec![quote!(foo: u32,)], field_initializers: vec![quote!(foo: ::derive_builder::export::core::default::Default::default(), )], functions: vec![quote!(fn bar() -> { unimplemented!() })], + generate_error: true, must_derive_clone: true, doc_comment: None, deprecation_notes: DeprecationNotes::default(), @@ -326,6 +335,45 @@ macro_rules! default_builder { mod tests { #[allow(unused_imports)] use super::*; + use proc_macro2::TokenStream; + + fn add_generated_error(result: &mut TokenStream) { + result.append_all(quote!( + #[doc="Error type for FooBuilder"] + #[derive(Debug)] + #[non_exhaustive] + pub enum FooBuilderError { + /// Uninitialized field + UninitializedField(&'static str), + /// Custom validation error + ValidationError(String), + } + + impl ::derive_builder::export::core::convert::From<::derive_builder::UninitializedFieldError> for FooBuilderError { + fn from(s: ::derive_builder::UninitializedFieldError) -> Self { + Self::UninitializedField(s.field_name()) + } + } + + impl ::derive_builder::export::core::convert::From for FooBuilderError { + fn from(s: String) -> Self { + Self::ValidationError(s) + } + } + + impl ::derive_builder::export::core::fmt::Display for FooBuilderError { + fn fmt(&self, f: &mut ::derive_builder::export::core::fmt::Formatter) -> ::derive_builder::export::core::fmt::Result { + match self { + Self::UninitializedField(ref field) => write!(f, "`{}` must be initialized", field), + Self::ValidationError(ref error) => write!(f, "{}", error), + } + } + } + + #[cfg(not(no_std))] + impl std::error::Error for FooBuilderError {} + )); + } #[test] fn simple() { @@ -346,42 +394,6 @@ mod tests { } )); - result.append_all(quote!( - #[doc="Error type for FooBuilder"] - #[derive(Debug)] - #[non_exhaustive] - pub enum FooBuilderError { - /// Uninitialized field - UninitializedField(&'static str), - /// Custom validation error - ValidationError(String), - } - - impl ::derive_builder::export::core::convert::From<&'static str> for FooBuilderError { - fn from(s: &'static str) -> Self { - Self::UninitializedField(s) - } - } - - impl ::derive_builder::export::core::convert::From for FooBuilderError { - fn from(s: String) -> Self { - Self::ValidationError(s) - } - } - - impl ::derive_builder::export::core::fmt::Display for FooBuilderError { - fn fmt(&self, f: &mut ::derive_builder::export::core::fmt::Formatter) -> ::derive_builder::export::core::fmt::Result { - match self { - Self::UninitializedField(ref field) => write!(f, "`{}` must be initialized", field), - Self::ValidationError(ref error) => write!(f, "{}", error), - } - } - } - - #[cfg(not(no_std))] - impl std::error::Error for FooBuilderError {} - )); - #[cfg(not(feature = "clippy"))] result.append_all(quote!(#[allow(clippy::all)])); @@ -402,6 +414,8 @@ mod tests { } )); + add_generated_error(&mut result); + result } .to_string() @@ -435,42 +449,6 @@ mod tests { } )); - result.append_all(quote!( - #[doc="Error type for FooBuilder"] - #[derive(Debug)] - #[non_exhaustive] - pub enum FooBuilderError { - /// Uninitialized field - UninitializedField(&'static str), - /// Custom validation error - ValidationError(String), - } - - impl ::derive_builder::export::core::convert::From<&'static str> for FooBuilderError { - fn from(s: &'static str) -> Self { - Self::UninitializedField(s) - } - } - - impl ::derive_builder::export::core::convert::From for FooBuilderError { - fn from(s: String) -> Self { - Self::ValidationError(s) - } - } - - impl ::derive_builder::export::core::fmt::Display for FooBuilderError { - fn fmt(&self, f: &mut ::derive_builder::export::core::fmt::Formatter) -> ::derive_builder::export::core::fmt::Result { - match self { - Self::UninitializedField(ref field) => write!(f, "`{}` must be initialized", field), - Self::ValidationError(ref error) => write!(f, "{}", error), - } - } - } - - #[cfg(not(no_std))] - impl std::error::Error for FooBuilderError {} - )); - #[cfg(not(feature = "clippy"))] result.append_all(quote!(#[allow(clippy::all)])); @@ -491,6 +469,8 @@ mod tests { } )); + add_generated_error(&mut result); + result }.to_string() ); @@ -524,42 +504,6 @@ mod tests { } )); - result.append_all(quote!( - #[doc="Error type for FooBuilder"] - #[derive(Debug)] - #[non_exhaustive] - pub enum FooBuilderError { - /// Uninitialized field - UninitializedField(&'static str), - /// Custom validation error - ValidationError(String), - } - - impl ::derive_builder::export::core::convert::From<&'static str> for FooBuilderError { - fn from(s: &'static str) -> Self { - Self::UninitializedField(s) - } - } - - impl ::derive_builder::export::core::convert::From for FooBuilderError { - fn from(s: String) -> Self { - Self::ValidationError(s) - } - } - - impl ::derive_builder::export::core::fmt::Display for FooBuilderError { - fn fmt(&self, f: &mut ::derive_builder::export::core::fmt::Formatter) -> ::derive_builder::export::core::fmt::Result { - match self { - Self::UninitializedField(ref field) => write!(f, "`{}` must be initialized", field), - Self::ValidationError(ref error) => write!(f, "{}", error), - } - } - } - - #[cfg(not(no_std))] - impl std::error::Error for FooBuilderError {} - )); - #[cfg(not(feature = "clippy"))] result.append_all(quote!(#[allow(clippy::all)])); @@ -583,6 +527,8 @@ mod tests { } )); + add_generated_error(&mut result); + result }.to_string() ); @@ -616,42 +562,6 @@ mod tests { } )); - result.append_all(quote!( - #[doc="Error type for FooBuilder"] - #[derive(Debug)] - #[non_exhaustive] - pub enum FooBuilderError { - /// Uninitialized field - UninitializedField(&'static str), - /// Custom validation error - ValidationError(String), - } - - impl ::derive_builder::export::core::convert::From<&'static str> for FooBuilderError { - fn from(s: &'static str) -> Self { - Self::UninitializedField(s) - } - } - - impl ::derive_builder::export::core::convert::From for FooBuilderError { - fn from(s: String) -> Self { - Self::ValidationError(s) - } - } - - impl ::derive_builder::export::core::fmt::Display for FooBuilderError { - fn fmt(&self, f: &mut ::derive_builder::export::core::fmt::Formatter) -> ::derive_builder::export::core::fmt::Result { - match self { - Self::UninitializedField(ref field) => write!(f, "`{}` must be initialized", field), - Self::ValidationError(ref error) => write!(f, "{}", error), - } - } - } - - #[cfg(not(no_std))] - impl std::error::Error for FooBuilderError {} - )); - #[cfg(not(feature = "clippy"))] result.append_all(quote!(#[allow(clippy::all)])); @@ -673,6 +583,8 @@ mod tests { } )); + add_generated_error(&mut result); + result }.to_string() ); @@ -707,42 +619,6 @@ mod tests { } )); - result.append_all(quote!( - #[doc="Error type for FooBuilder"] - #[derive(Debug)] - #[non_exhaustive] - pub enum FooBuilderError { - /// Uninitialized field - UninitializedField(&'static str), - /// Custom validation error - ValidationError(String), - } - - impl ::derive_builder::export::core::convert::From<&'static str> for FooBuilderError { - fn from(s: &'static str) -> Self { - Self::UninitializedField(s) - } - } - - impl ::derive_builder::export::core::convert::From for FooBuilderError { - fn from(s: String) -> Self { - Self::ValidationError(s) - } - } - - impl ::derive_builder::export::core::fmt::Display for FooBuilderError { - fn fmt(&self, f: &mut ::derive_builder::export::core::fmt::Formatter) -> ::derive_builder::export::core::fmt::Result { - match self { - Self::UninitializedField(ref field) => write!(f, "`{}` must be initialized", field), - Self::ValidationError(ref error) => write!(f, "{}", error), - } - } - } - - #[cfg(not(no_std))] - impl std::error::Error for FooBuilderError {} - )); - #[cfg(not(feature = "clippy"))] result.append_all(quote!(#[allow(clippy::all)])); @@ -763,6 +639,8 @@ mod tests { } )); + add_generated_error(&mut result); + result } .to_string() diff --git a/derive_builder_core/src/error.rs b/derive_builder_core/src/error.rs new file mode 100644 index 00000000..6446da38 --- /dev/null +++ b/derive_builder_core/src/error.rs @@ -0,0 +1,32 @@ +use std::{error::Error, fmt}; + +/// Runtime error when a `build()` method is called and one or more required fields +/// do not have a value. +#[derive(Debug, Clone)] +pub struct UninitializedFieldError(&'static str); + +impl UninitializedFieldError { + /// Create a new `UnitializedFieldError` for the specified field name. + pub fn new(field_name: &'static str) -> Self { + UninitializedFieldError(field_name) + } + + /// Get the name of the first-declared field that wasn't initialized + pub fn field_name(&self) -> &'static str { + self.0 + } +} + +impl fmt::Display for UninitializedFieldError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Field not initialized: {}", self.0) + } +} + +impl Error for UninitializedFieldError {} + +impl From<&'static str> for UninitializedFieldError { + fn from(field_name: &'static str) -> Self { + Self::new(field_name) + } +} diff --git a/derive_builder_core/src/initializer.rs b/derive_builder_core/src/initializer.rs index 8cfe52fd..d75d909b 100644 --- a/derive_builder_core/src/initializer.rs +++ b/derive_builder_core/src/initializer.rs @@ -48,6 +48,16 @@ pub struct Initializer<'a> { pub default_value: Option, /// Whether the build_method defines a default struct. pub use_default_struct: bool, + /// Span where the macro was told to use a preexisting error type, instead of creating one, + /// to represent failures of the `build` method. + /// + /// An initializer can force early-return if a field has no set value and no default is + /// defined. In these cases, it will convert from `derive_builder::UninitializedFieldError` + /// into the return type of its enclosing `build` method. That conversion is guaranteed to + /// work for generated error types, but if the caller specified an error type to use instead + /// they may have forgotten the conversion from `UninitializedFieldError` into their specified + /// error type. + pub custom_error_type_span: Option, } impl<'a> ToTokens for Initializer<'a> { @@ -90,7 +100,10 @@ impl<'a> Initializer<'a> { if self.use_default_struct { MatchNone::UseDefaultStructField(self.field_ident) } else { - MatchNone::ReturnError(self.field_ident.to_string()) + MatchNone::ReturnError( + self.field_ident.to_string(), + self.custom_error_type_span, + ) } } } @@ -118,7 +131,7 @@ enum MatchNone<'a> { /// The default struct must be in scope in the build_method. UseDefaultStructField(&'a syn::Ident), /// Inner value must be the field name - ReturnError(String), + ReturnError(String, Option), } impl<'a> ToTokens for MatchNone<'a> { @@ -133,9 +146,15 @@ impl<'a> ToTokens for MatchNone<'a> { None => #struct_ident.#field_ident )) } - MatchNone::ReturnError(ref err) => tokens.append_all(quote!( - None => return ::derive_builder::export::core::result::Result::Err(::derive_builder::export::core::convert::Into::into(#err)) - )), + MatchNone::ReturnError(ref field_name, ref span) => { + let conv_span = span.unwrap_or_else(Span::call_site); + let err_conv = quote_spanned!(conv_span => ::derive_builder::export::core::convert::Into::into( + ::derive_builder::UninitializedFieldError::from(#field_name) + )); + tokens.append_all(quote!( + None => return ::derive_builder::export::core::result::Result::Err(#err_conv) + )); + } } } } @@ -171,6 +190,7 @@ macro_rules! default_initializer { builder_pattern: BuilderPattern::Mutable, default_value: None, use_default_struct: false, + custom_error_type_span: None, } }; } @@ -191,7 +211,7 @@ mod tests { foo: match self.foo { Some(ref value) => ::derive_builder::export::core::clone::Clone::clone(value), None => return ::derive_builder::export::core::result::Result::Err(::derive_builder::export::core::convert::Into::into( - "foo" + ::derive_builder::UninitializedFieldError::from("foo") )), }, ) @@ -210,7 +230,7 @@ mod tests { foo: match self.foo { Some(ref value) => ::derive_builder::export::core::clone::Clone::clone(value), None => return ::derive_builder::export::core::result::Result::Err(::derive_builder::export::core::convert::Into::into( - "foo" + ::derive_builder::UninitializedFieldError::from("foo") )), }, ) @@ -229,7 +249,7 @@ mod tests { foo: match self.foo { Some(value) => value, None => return ::derive_builder::export::core::result::Result::Err(::derive_builder::export::core::convert::Into::into( - "foo" + ::derive_builder::UninitializedFieldError::from("foo") )), }, ) @@ -292,7 +312,7 @@ mod tests { foo: match self.foo { Some(ref value) => ::derive_builder::export::core::clone::Clone::clone(value), None => return ::derive_builder::export::core::result::Result::Err(::derive_builder::export::core::convert::Into::into( - "foo" + ::derive_builder::UninitializedFieldError::from("foo") )), }, ) diff --git a/derive_builder_core/src/lib.rs b/derive_builder_core/src/lib.rs index 2970593b..5294b203 100644 --- a/derive_builder_core/src/lib.rs +++ b/derive_builder_core/src/lib.rs @@ -37,11 +37,14 @@ mod builder; mod builder_field; mod deprecation_notes; mod doc_comment; +mod error; mod initializer; mod macro_options; mod options; mod setter; +pub use error::UninitializedFieldError; + pub(crate) use block::Block; pub(crate) use build_method::BuildMethod; pub(crate) use builder::Builder; diff --git a/derive_builder_core/src/macro_options/darling_opts.rs b/derive_builder_core/src/macro_options/darling_opts.rs index 7343220e..5c59a729 100644 --- a/derive_builder_core/src/macro_options/darling_opts.rs +++ b/derive_builder_core/src/macro_options/darling_opts.rs @@ -5,7 +5,7 @@ use crate::BuildMethod; use darling::util::{Flag, PathList}; use darling::{self, FromMeta}; use proc_macro2::Span; -use syn::{self, Attribute, Generics, Ident, Path, Visibility}; +use syn::{self, spanned::Spanned, Attribute, Generics, Ident, Path, Visibility}; use crate::macro_options::DefaultExpression; use crate::{Builder, BuilderField, BuilderPattern, DeprecationNotes, Initializer, Setter}; @@ -44,6 +44,20 @@ pub struct BuildFn { validate: Option, public: Flag, private: Flag, + /// The path to an existing error type that the build method should return. + /// + /// Setting this will prevent `derive_builder` from generating an error type for the build + /// method. + /// + /// # Type Bounds + /// This type's bounds depend on other settings of the builder. + /// + /// * If uninitialized fields cause `build()` to fail, then this type + /// must `impl From`. Uninitialized fields do not cause errors + /// when default values are provided for every field or at the struct level. + /// * If `validate` is specified, then this type must provide a conversion from the specified + /// function's error type. + error: Option, } impl Default for BuildFn { @@ -54,6 +68,7 @@ impl Default for BuildFn { validate: None, public: Default::default(), private: Default::default(), + error: None, } } } @@ -317,11 +332,13 @@ impl Options { .expect("Struct name with Builder suffix should be an ident") } - pub fn builder_error_ident(&self) -> Ident { - if let Some(ref custom) = self.name { - format_ident!("{}Error", custom) + pub fn builder_error_ident(&self) -> Path { + if let Some(existing) = self.build_fn.error.as_ref() { + existing.clone() + } else if let Some(ref custom) = self.name { + format_ident!("{}Error", custom).into() } else { - format_ident!("{}BuilderError", self.ident) + format_ident!("{}BuilderError", self.ident).into() } } @@ -378,6 +395,7 @@ impl Options { fields: Vec::with_capacity(self.field_count()), field_initializers: Vec::with_capacity(self.field_count()), functions: Vec::with_capacity(self.field_count()), + generate_error: self.build_fn.error.is_none(), must_derive_clone: self.requires_clone(), doc_comment: None, deprecation_notes: Default::default(), @@ -556,6 +574,12 @@ impl<'a> FieldWithDefaults<'a> { .as_ref() .map(|x| x.parse_block(self.parent.no_std.into())), use_default_struct: self.use_parent_default(), + custom_error_type_span: self + .parent + .build_fn + .error + .as_ref() + .map(|err_ty| err_ty.span()), } }