Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add UninitializedFieldError Structured errors need a safe way to act on uninitialized fields that doesn't conflate with validation errors. This commit adds that by providing a struct to represent an uninitialized field and generating conversions from that into the auto-generated error type. * Attach validation-related errors to better span If the build() method cannot convert the validation error to the generated builder error, this is a problem with the validation function. The error span should point there, not at the macro call-site. * Allow specification of preexisting error Using `#[builder(build_fn(error = "..."))]` allows passing an error type to use instead of generating one. This makes it possible to integrate with a crate's existing error. * Add run-pass custom error test This test checks that we don't require unnecessary impls for custom errors when uninitialized fields are impossible. Fixes #181 Fixes #191
- Loading branch information
Showing
15 changed files
with
463 additions
and
235 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<UninitializedFieldError> 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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<N>"))] | ||
struct Person<N: Popular + Clone> { | ||
name: N, | ||
age: u16, | ||
} | ||
|
||
#[derive(Debug)] | ||
enum Error<N> { | ||
UninitializedField(&'static str), | ||
UnpopularName(N), | ||
} | ||
|
||
impl<N> From<UninitializedFieldError> for Error<N> { | ||
fn from(error: UninitializedFieldError) -> Self { | ||
Self::UninitializedField(error.field_name()) | ||
} | ||
} | ||
|
||
fn check_person<N: Popular + Clone>(builder: &PersonBuilder<N>) -> Result<(), Error<N>> { | ||
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()); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
derive_builder/tests/compile-fail/custom_error_generic_missing_bound.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<N>"))] | ||
struct Person<N> { | ||
name: N, | ||
age: u16, | ||
} | ||
|
||
enum Error<N> { | ||
UninitializedField(&'static str), | ||
UnpopularName(N), | ||
} | ||
|
||
impl<N> From<UninitializedFieldError> for Error<N> { | ||
fn from(error: UninitializedFieldError) -> Self { | ||
Self::UninitializedField(error.field_name()) | ||
} | ||
} | ||
|
||
fn check_person<N: Popular + Clone>(builder: &PersonBuilder<N>) -> Result<(), Error<N>> { | ||
if let Some(name) = &builder.name { | ||
if !name.is_popular() { | ||
return Err(Error::UnpopularName(name.clone())); | ||
} | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
fn main() {} |
10 changes: 10 additions & 0 deletions
10
derive_builder/tests/compile-fail/custom_error_generic_missing_bound.stderr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<N>"))] | ||
| ^^^^^^^^^^^^^^ the trait `Popular` is not implemented for `N` | ||
18 | struct Person<N> { | ||
| - consider adding a `where N: Popular` bound | ||
... | ||
34 | fn check_person<N: Popular + Clone>(builder: &PersonBuilder<N>) -> Result<(), Error<N>> { | ||
| ------------ ------- required by this bound in `check_person` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<derive_builder::UninitializedFieldError>`, which is a | ||
// compile-blocking mistake. | ||
#[derive(Debug)] | ||
enum Error { | ||
/// A required field is not filled out. | ||
MissingData(&'static str), | ||
UnrealisticAge(usize), | ||
} | ||
|
||
fn main() {} |
8 changes: 8 additions & 0 deletions
8
derive_builder/tests/compile-fail/custom_error_no_from.stderr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
error[E0277]: the trait bound `Error: std::convert::From<derive_builder::UninitializedFieldError>` 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<derive_builder::UninitializedFieldError>` is not implemented for `Error` | ||
| | ||
= note: required because of the requirements on the impl of `std::convert::Into<Error>` for `derive_builder::UninitializedFieldError` | ||
= note: required by `std::convert::Into::into` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<UninitializedFieldError>`; 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() {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.