Skip to content

Commit

Permalink
Support derive(FromStr) for generics with validation
Browse files Browse the repository at this point in the history
  • Loading branch information
greyblake committed Jun 26, 2024
1 parent 11ff048 commit 16e9fe6
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 39 deletions.
70 changes: 68 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions dummy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ once_cell = "*"
lazy_static = "*"
ron = "0.8.1"
arbitrary = "1.3.2"
num = "0.4.3"
23 changes: 4 additions & 19 deletions dummy/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,9 @@
use nutype::nutype;
use std::cmp::Ord;

#[nutype(
sanitize(with = |mut v| { v.sort(); v }),
validate(predicate = |vec| !vec.is_empty()),
derive(Debug, Deserialize, Serialize),
validate(predicate = |n| n.is_even()),
derive(Debug, FromStr),
)]
struct SortedNotEmptyVec<T: Ord>(Vec<T>);
struct Even<T: ::num::Integer>(T);

fn main() {
{
// Not empty vec is fine
let json = "[3, 1, 5, 2]";
let sv = serde_json::from_str::<SortedNotEmptyVec<i32>>(json).unwrap();
assert_eq!(sv.into_inner(), vec![1, 2, 3, 5]);
}
{
// Empty vec is not allowed
let json = "[]";
let result = serde_json::from_str::<SortedNotEmptyVec<i32>>(json);
assert!(result.is_err());
}
}
fn main() {}
15 changes: 7 additions & 8 deletions nutype_macros/src/common/gen/parse_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,17 @@ pub fn gen_def_parse_error(
);

let definition = if let Some(error_type_name) = maybe_error_type_name {
// TODO! Use generics here too!
quote! {
#[derive(Debug)]
pub enum #parse_error_type_name {
Parse(<#inner_type as ::core::str::FromStr>::Err),
Validate(#error_type_name),
}
#[derive(Debug)] // #[derive(Debug)]
pub enum #parse_error_type_name #generics_with_fromstr_bound { // pub enum ParseErrorFoo<T: ::core::str::FromStr<Err: ::core::fmt::Debug>> {
Parse(<#inner_type as ::core::str::FromStr>::Err), // Parse(<Foo as ::core::str::FromStr>::Err),
Validate(#error_type_name), // Validate(ErrorFoo),
} // }

impl ::core::fmt::Display for #parse_error_type_name {
impl #generics_with_fromstr_bound ::core::fmt::Display for #parse_error_type_name #generics_without_bounds {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
match self {
#parse_error_type_name::Parse(err) => write!(f, "Failed to parse {}: {}", #type_name_str, err),
#parse_error_type_name::Parse(err) => write!(f, "Failed to parse {}: {:?}", #type_name_str, err),
#parse_error_type_name::Validate(err) => write!(f, "Failed to parse {}: {}", #type_name_str, err),
}

Expand Down
1 change: 1 addition & 0 deletions test_suite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ arbitrary = "1.3.0"
arbtest = "0.2.0"
ron = "0.8.1"
rmp-serde = "1.1.2"
num = "0.4.3"

[features]
serde = ["nutype/serde", "dep:serde", "dep:serde_json"]
Expand Down
36 changes: 28 additions & 8 deletions test_suite/tests/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ mod traits {
let err = "6,5,4".parse::<Position>().unwrap_err();
assert_eq!(
err.to_string(),
"Failed to parse Position: Point must be two comma separated integers"
"Failed to parse Position: \"Point must be two comma separated integers\""
);
}

Expand Down Expand Up @@ -669,7 +669,7 @@ mod with_generics {
}

#[test]
fn test_generic_boundaries_try_from_without_validation() {
fn test_generic_try_from_without_validation() {
// Note, that we get TryFrom thanks to the blanket implementation in core:
//
// impl<T, U> TryFrom<U> for T
Expand All @@ -684,7 +684,7 @@ mod with_generics {
}

#[test]
fn test_generic_boundaries_try_from_with_validation() {
fn test_generic_try_from_with_validation() {
#[nutype(
derive(Debug, TryFrom),
validate(predicate = |v| !v.is_empty())
Expand All @@ -701,7 +701,7 @@ mod with_generics {
}

#[test]
fn test_generic_boundaries_from_str() {
fn test_generic_from_str_without_validation() {
#[nutype(derive(Debug, FromStr))]
struct Parseable<T>(T);

Expand All @@ -722,13 +722,33 @@ mod with_generics {
"Failed to parse Parseable: ParseIntError { kind: InvalidDigit }"
);
}

{
let four = "4".parse::<Parseable<Parseable<i32>>>().unwrap();
assert_eq!(four.into_inner().into_inner(), 4);
}
}

#[test]
fn test_generic_boundaries_from_str_with_lifetime() {
// TODO
// #[nutype(derive(FromStr))]
// struct Clarabelle<'a>(Cow<'a, str>);
fn test_generic_from_str_with_validation() {
#[nutype(
validate(predicate = |n| n.is_even()),
derive(Debug, FromStr),
)]
struct Even<T: ::num::Integer>(T);

{
let err = "13".parse::<Even<i32>>().unwrap_err();
assert_eq!(
err.to_string(),
"Failed to parse Even: Even failed the predicate test."
);
}

{
let twelve = "12".parse::<Even<i32>>().unwrap();
assert_eq!(twelve.into_inner(), 12);
}
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion test_suite/tests/float.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ mod traits {
let err: DistParseError = "foobar".parse::<Dist>().unwrap_err();
assert_eq!(
err.to_string(),
"Failed to parse Dist: invalid float literal"
"Failed to parse Dist: ParseFloatError { kind: Invalid }"
);

// Unhappy path: validation error
Expand Down
2 changes: 1 addition & 1 deletion test_suite/tests/integer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ mod traits {
let err: AgeParseError = "foobar".parse::<Age>().unwrap_err();
assert_eq!(
err.to_string(),
"Failed to parse Age: invalid digit found in string"
"Failed to parse Age: ParseIntError { kind: InvalidDigit }"
);

// Unhappy path: validation error
Expand Down

0 comments on commit 16e9fe6

Please sign in to comment.