diff --git a/derive_builder/build/skeptic.rs b/derive_builder/build/skeptic.rs index 3119eb31..4c0ebe4b 100644 --- a/derive_builder/build/skeptic.rs +++ b/derive_builder/build/skeptic.rs @@ -62,7 +62,7 @@ use std::io::{Write, Read}; const DOC_TPL_DIR: &'static str = "src/doc_tpl/"; const DOC_TPL_OUT_DIR: &'static str = "doc_tpl/"; -fn generate_doc_tpl_tests() -> Result, Box> { +fn generate_doc_tpl_tests() -> Result, Box> { trace!("Generating doc template tests"); let root_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); let mut tpl_dir = root_dir; diff --git a/derive_builder/examples/validation.rs b/derive_builder/examples/validation.rs index ebbaf835..154b937e 100644 --- a/derive_builder/examples/validation.rs +++ b/derive_builder/examples/validation.rs @@ -30,5 +30,5 @@ fn main() { let x = LoremBuilder::default().ipsum(120).build().unwrap_err(); // .. the build will fail: - assert_eq!(&x, "You'll tire yourself out"); + assert_eq!(&x.to_string(), "You'll tire yourself out"); } diff --git a/derive_builder/src/lib.rs b/derive_builder/src/lib.rs index 148e9e8c..916055e8 100644 --- a/derive_builder/src/lib.rs +++ b/derive_builder/src/lib.rs @@ -63,7 +63,8 @@ //! //! You can easily opt into different patterns and control many other aspects. //! -//! The build method returns `Result`, where `T` is the struct you started with. +//! The build method returns `Result`, where `T` is the struct you started with +//! and E is a generated builder error type. //! It returns `Err` if you didn't initialize all fields and no default values were //! provided. //! @@ -74,7 +75,7 @@ //! ```rust //! # #[macro_use] extern crate derive_builder; //! # #[derive(Builder)] struct Lorem { ipsum: u32 } -//! # fn try_main() -> Result<(), String> { +//! # fn try_main() -> Result<(), Box> { //! let x: Lorem = LoremBuilder::default().ipsum(42).build()?; //! # Ok(()) //! # } fn main() { try_main().unwrap(); } @@ -87,7 +88,7 @@ //! ```rust //! # #[macro_use] extern crate derive_builder; //! # #[derive(Builder)] struct Lorem { ipsum: u32 } -//! # fn try_main() -> Result<(), String> { +//! # fn try_main() -> Result<(), Box> { //! # let geek = true; //! let mut builder = LoremBuilder::default(); //! if geek { @@ -439,7 +440,7 @@ //! let x = LoremBuilder::default().ipsum(120).build().unwrap_err(); //! //! // .. the build will fail: -//! assert_eq!(&x, "You'll tire yourself out"); +//! assert_eq!(&x.to_string(), "You'll tire yourself out"); //! } //! ``` //! diff --git a/derive_builder/src/options/darling_opts.rs b/derive_builder/src/options/darling_opts.rs index 1616b773..cd5e45c6 100644 --- a/derive_builder/src/options/darling_opts.rs +++ b/derive_builder/src/options/darling_opts.rs @@ -319,6 +319,14 @@ 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) + } else { + format_ident!("{}BuilderError", self.ident) + } + } + /// The visibility of the builder struct. /// If a visibility was declared in attributes, that will be used; /// otherwise the struct's own visibility will be used. @@ -393,6 +401,7 @@ impl Options { pattern: self.pattern, target_ty: &self.ident, target_ty_generics: Some(ty_generics), + error_ty: self.builder_error_ident(), initializers: Vec::with_capacity(self.field_count()), doc_comment: None, bindings: self.bindings(), diff --git a/derive_builder/tests/builder_name.rs b/derive_builder/tests/builder_name.rs index 70c0bd69..3b2cd570 100644 --- a/derive_builder/tests/builder_name.rs +++ b/derive_builder/tests/builder_name.rs @@ -13,9 +13,9 @@ struct Lorem { } #[test] -#[should_panic(expected = "`ipsum` must be initialized")] -fn panic_if_uninitialized() { - MyBuilder::default().build().unwrap(); +fn error_if_uninitialized() { + let error = MyBuilder::default().build().unwrap_err(); + assert_eq!(&error.to_string(), "`ipsum` must be initialized"); } #[test] diff --git a/derive_builder/tests/custom_default.rs b/derive_builder/tests/custom_default.rs index 943faa7e..b2dfbf66 100644 --- a/derive_builder/tests/custom_default.rs +++ b/derive_builder/tests/custom_default.rs @@ -24,9 +24,9 @@ mod field_level { } #[test] - #[should_panic(expected = "`required` must be initialized")] - fn panic_if_uninitialized() { - LoremBuilder::default().build().unwrap(); + fn error_if_uninitialized() { + let error = LoremBuilder::default().build().unwrap_err(); + assert_eq!(&error.to_string(), "`required` must be initialized"); } #[test] diff --git a/derive_builder/tests/generic_structs.rs b/derive_builder/tests/generic_structs.rs index 26984992..f5119f55 100644 --- a/derive_builder/tests/generic_structs.rs +++ b/derive_builder/tests/generic_structs.rs @@ -24,9 +24,9 @@ where } #[test] -#[should_panic(expected = "`ipsum` must be initialized")] -fn panic_if_uninitialized() { - GenericBuilder::::default().build().unwrap(); +fn error_if_uninitialized() { + let error = GenericBuilder::::default().build().unwrap_err(); + assert_eq!(&error.to_string(), "`ipsum` must be initialized"); } #[test] diff --git a/derive_builder/tests/lifetime.rs b/derive_builder/tests/lifetime.rs index 264e4ed3..13f2a08e 100644 --- a/derive_builder/tests/lifetime.rs +++ b/derive_builder/tests/lifetime.rs @@ -9,9 +9,9 @@ struct Lorem<'a> { } #[test] -#[should_panic(expected = "`ipsum` must be initialized")] -fn panic_if_uninitialized() { - LoremBuilder::default().build().unwrap(); +fn error_if_uninitialized() { + let error = LoremBuilder::default().build().unwrap_err(); + assert_eq!(&error.to_string(), "`ipsum` must be initialized"); } #[test] diff --git a/derive_builder/tests/run-pass/custom_types.rs b/derive_builder/tests/run-pass/custom_types.rs index 8f8daaa4..2b4f999e 100644 --- a/derive_builder/tests/run-pass/custom_types.rs +++ b/derive_builder/tests/run-pass/custom_types.rs @@ -3,12 +3,26 @@ #[macro_use] extern crate derive_builder; -type Clone = (); -type Into = (); -type Option = (); -type Result = (); -type Some = (); -type String = (); +struct Unit; + +type Clone = Unit; +type Into = Unit; +type Option = Unit; +type Result = Unit; +type Some = Unit; +type String = Unit; + +impl core::fmt::Debug for Unit { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "()") + } +} + +impl core::fmt::Display for Unit { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "()") + } +} #[derive(Builder)] struct IgnoreEmptyStruct {} diff --git a/derive_builder/tests/setter_visibility.rs b/derive_builder/tests/setter_visibility.rs index 71ae0866..99950934 100644 --- a/derive_builder/tests/setter_visibility.rs +++ b/derive_builder/tests/setter_visibility.rs @@ -53,12 +53,11 @@ pub mod foo { } #[test] -#[should_panic(expected = "`private` must be initialized")] fn public_setters_foreign_module() { - let y = foo::IpsumBuilder::default() + let error = foo::IpsumBuilder::default() .public("Hello") .build() - .unwrap(); + .unwrap_err(); - assert_eq!(y.public, "Hello".to_string()); + assert_eq!(&error.to_string(), "`private` must be initialized"); } diff --git a/derive_builder/tests/try_setter.rs b/derive_builder/tests/try_setter.rs index 7b47babf..28f6a01c 100644 --- a/derive_builder/tests/try_setter.rs +++ b/derive_builder/tests/try_setter.rs @@ -36,14 +36,14 @@ struct Ipsum { pub source: MyAddr, } -fn exact_helper() -> Result { +fn exact_helper() -> Result { LoremBuilder::default() .source(IpAddr::from_str("1.2.3.4").unwrap()) .dest(IpAddr::from_str("0.0.0.0").unwrap()) .build() } -fn try_helper() -> Result { +fn try_helper() -> Result { LoremBuilder::default() .try_source("1.2.3.4") .map_err(|e| e.to_string())? diff --git a/derive_builder/tests/validation.rs b/derive_builder/tests/validation.rs index ce721c2f..87776428 100644 --- a/derive_builder/tests/validation.rs +++ b/derive_builder/tests/validation.rs @@ -43,14 +43,19 @@ impl LoremBuilder { #[test] fn out_of_bounds() { assert_eq!( - &LoremBuilder::default().my_effort(120).build().unwrap_err(), + &LoremBuilder::default() + .my_effort(120) + .build() + .unwrap_err() + .to_string(), "Don't wear yourself out" ); assert_eq!( &LoremBuilder::default() .rivals_effort(120) .build() - .unwrap_err(), + .unwrap_err() + .to_string(), "Your rival is cheating" ); } diff --git a/derive_builder_core/src/build_method.rs b/derive_builder_core/src/build_method.rs index 58d46b59..ec0fbff3 100644 --- a/derive_builder_core/src/build_method.rs +++ b/derive_builder_core/src/build_method.rs @@ -27,7 +27,7 @@ use DEFAULT_STRUCT_NAME; /// # let build_method = default_build_method!(); /// # /// # assert_eq!(quote!(#build_method).to_string(), quote!( -/// pub fn build(&self) -> ::std::result::Result { +/// pub fn build(&self) -> ::std::result::Result { /// Ok(Foo { /// foo: self.foo, /// }) @@ -51,6 +51,8 @@ pub struct BuildMethod<'a> { pub target_ty: &'a syn::Ident, /// Type parameters and lifetimes attached to this builder struct. pub target_ty_generics: Option>, + /// Type of error. + pub error_ty: syn::Ident, /// Field initializers for the target type. pub initializers: Vec, /// Doc-comment of the builder struct. @@ -84,14 +86,14 @@ impl<'a> ToTokens for BuildMethod<'a> { }); let validate_fn = self.validate_fn.as_ref().map(|vfn| quote!(#vfn(&self)?;)); let result = self.bindings.result_ty(); - let string = self.bindings.string_ty(); + let error_ty = &self.error_ty; if self.enabled { trace!("Deriving build method `{}`.", self.ident); tokens.append_all(quote!( #doc_comment #vis fn #ident(#self_param) - -> #result<#target_ty #target_ty_generics, #string> + -> #result<#target_ty #target_ty_generics, #error_ty> { #validate_fn #default_struct @@ -137,6 +139,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()), initializers: vec![quote!(foo: self.foo,)], doc_comment: None, bindings: Default::default(), @@ -159,7 +162,7 @@ mod tests { assert_eq!( quote!(#build_method).to_string(), quote!( - pub fn build(&self) -> ::std::result::Result { + pub fn build(&self) -> ::std::result::Result { Ok(Foo { foo: self.foo, }) @@ -178,7 +181,7 @@ mod tests { assert_eq!( quote!(#build_method).to_string(), quote!( - pub fn build(&self) -> ::core::result::Result { + pub fn build(&self) -> ::core::result::Result { Ok(Foo { foo: self.foo, }) @@ -197,7 +200,7 @@ mod tests { assert_eq!( quote!(#build_method).to_string(), quote!( - pub fn build(&self) -> ::std::result::Result { + pub fn build(&self) -> ::std::result::Result { let __default: Foo = { Default::default() }; Ok(Foo { foo: self.foo, @@ -227,7 +230,7 @@ mod tests { assert_eq!( quote!(#build_method).to_string(), quote!( - pub fn finish(&self) -> ::std::result::Result { + pub fn finish(&self) -> ::std::result::Result { Ok(Foo { foo: self.foo, }) @@ -249,7 +252,7 @@ mod tests { assert_eq!( quote!(#build_method).to_string(), quote!( - pub fn build(&self) -> ::std::result::Result { + pub fn build(&self) -> ::std::result::Result { IpsumBuilder::validate(&self)?; Ok(Foo { diff --git a/derive_builder_core/src/builder.rs b/derive_builder_core/src/builder.rs index 18340087..83b7b4fe 100644 --- a/derive_builder_core/src/builder.rs +++ b/derive_builder_core/src/builder.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream; -use quote::{ToTokens, TokenStreamExt}; +use quote::{format_ident, ToTokens, TokenStreamExt}; use syn::punctuated::Punctuated; use syn::{self, Path, TraitBound, TraitBoundModifier, TypeParamBound}; @@ -41,6 +41,40 @@ use Setter; /// pub struct FooBuilder { /// foo: u32, /// } +/// +/// #[doc="Error type for FooBuilder"] +/// #[derive(Debug)] +/// #[non_exhaustive] +/// pub enum FooBuilderError { +/// /// Uninitialized field +/// UninitializedField(&'static str), +/// /// Custom validation error +/// ValidationError(String), +/// } +/// +/// impl core::convert::From<&'static str> for FooBuilderError { +/// fn from(s: &'static str) -> Self { +/// Self::UninitializedField(s) +/// } +/// } +/// +/// impl core::convert::From for FooBuilderError { +/// fn from(s: String) -> Self { +/// Self::ValidationError(s) +/// } +/// } +/// +/// impl core::fmt::Display for FooBuilderError { +/// fn fmt(&self, f: &mut core::fmt::Formatter) -> 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)])); @@ -136,12 +170,49 @@ 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(#derived_traits)] #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 core::convert::From<&'static str> for #builder_error_ident { + fn from(s: &'static str) -> Self { + Self::UninitializedField(s) + } + } + + impl core::convert::From for #builder_error_ident { + fn from(s: String) -> Self { + Self::ValidationError(s) + } + } + + impl core::fmt::Display for #builder_error_ident { + fn fmt(&self, f: &mut core::fmt::Formatter) -> 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"))] @@ -262,6 +333,42 @@ 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 core::convert::From<&'static str> for FooBuilderError { + fn from(s: &'static str) -> Self { + Self::UninitializedField(s) + } + } + + impl core::convert::From for FooBuilderError { + fn from(s: String) -> Self { + Self::ValidationError(s) + } + } + + impl core::fmt::Display for FooBuilderError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> 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)])); @@ -307,6 +414,42 @@ 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 core::convert::From<&'static str> for FooBuilderError { + fn from(s: &'static str) -> Self { + Self::UninitializedField(s) + } + } + + impl core::convert::From for FooBuilderError { + fn from(s: String) -> Self { + Self::ValidationError(s) + } + } + + impl core::fmt::Display for FooBuilderError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> 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)])); @@ -352,6 +495,42 @@ 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 core::convert::From<&'static str> for FooBuilderError { + fn from(s: &'static str) -> Self { + Self::UninitializedField(s) + } + } + + impl core::convert::From for FooBuilderError { + fn from(s: String) -> Self { + Self::ValidationError(s) + } + } + + impl core::fmt::Display for FooBuilderError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> 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)])); @@ -401,6 +580,42 @@ 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 core::convert::From<&'static str> for FooBuilderError { + fn from(s: &'static str) -> Self { + Self::UninitializedField(s) + } + } + + impl core::convert::From for FooBuilderError { + fn from(s: String) -> Self { + Self::ValidationError(s) + } + } + + impl core::fmt::Display for FooBuilderError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> 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)])); @@ -447,6 +662,42 @@ 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 core::convert::From<&'static str> for FooBuilderError { + fn from(s: &'static str) -> Self { + Self::UninitializedField(s) + } + } + + impl core::convert::From for FooBuilderError { + fn from(s: String) -> Self { + Self::ValidationError(s) + } + } + + impl core::fmt::Display for FooBuilderError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> 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)])); diff --git a/derive_builder_core/src/initializer.rs b/derive_builder_core/src/initializer.rs index 329c89ee..9026c523 100644 --- a/derive_builder_core/src/initializer.rs +++ b/derive_builder_core/src/initializer.rs @@ -101,12 +101,9 @@ impl<'a> Initializer<'a> { if self.use_default_struct { MatchNone::UseDefaultStructField(self.field_ident) } else if self.bindings.no_std { - MatchNone::ReturnErrorNoStd(format!( - "`{}` must be initialized", - self.field_ident - )) + MatchNone::ReturnErrorNoStd(self.field_ident.to_string()) } else { - MatchNone::ReturnError(format!("`{}` must be initialized", self.field_ident)) + MatchNone::ReturnError(self.field_ident.to_string()) } } } @@ -155,11 +152,11 @@ impl<'a> ToTokens for MatchNone<'a> { )) } MatchNone::ReturnError(ref err) => tokens.append_all(quote!( - None => return ::std::result::Result::Err(::std::string::String::from(#err)) + None => return ::std::result::Result::Err(::std::convert::Into::into(#err)) )), MatchNone::ReturnErrorNoStd(ref err) => tokens.append_all(quote!( None => return ::core::result::Result::Err( - ::alloc::string::String::from(#err)) + ::std::convert::Into::into(#err)) )), } } @@ -220,8 +217,8 @@ mod tests { quote!( foo: match self.foo { Some(ref value) => ::std::clone::Clone::clone(value), - None => return ::std::result::Result::Err(::std::string::String::from( - "`foo` must be initialized" + None => return ::std::result::Result::Err(::std::convert::Into::into( + "foo" )), }, ) @@ -239,8 +236,8 @@ mod tests { quote!( foo: match self.foo { Some(ref value) => ::std::clone::Clone::clone(value), - None => return ::std::result::Result::Err(::std::string::String::from( - "`foo` must be initialized" + None => return ::std::result::Result::Err(::std::convert::Into::into( + "foo" )), }, ) @@ -258,8 +255,8 @@ mod tests { quote!( foo: match self.foo { Some(value) => value, - None => return ::std::result::Result::Err(::std::string::String::from( - "`foo` must be initialized" + None => return ::std::result::Result::Err(::std::convert::Into::into( + "foo" )), }, ) @@ -322,8 +319,8 @@ mod tests { quote!( foo: match self.foo { Some(ref value) => ::core::clone::Clone::clone(value), - None => return ::core::result::Result::Err(::alloc::string::String::from( - "`foo` must be initialized" + None => return ::core::result::Result::Err(::std::convert::Into::into( + "foo" )), }, )