From 11ff048791579e3d6924fb6fde93b75d09b601bc Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sun, 23 Jun 2024 21:11:07 +0200 Subject: [PATCH 1/2] Start implementing support of derive(FromStr) for generic newtypes --- nutype_macros/src/any/gen/traits/mod.rs | 2 +- nutype_macros/src/common/gen/parse_error.rs | 34 ++++++++++++++------ nutype_macros/src/common/gen/traits.rs | 18 ++++++++--- nutype_macros/src/float/gen/traits/mod.rs | 2 +- nutype_macros/src/integer/gen/traits/mod.rs | 2 +- test_suite/tests/any.rs | 35 ++++++++++++++++++--- test_suite/tests/float.rs | 2 +- test_suite/tests/integer.rs | 2 +- 8 files changed, 73 insertions(+), 24 deletions(-) diff --git a/nutype_macros/src/any/gen/traits/mod.rs b/nutype_macros/src/any/gen/traits/mod.rs index 7d98909..1d36716 100644 --- a/nutype_macros/src/any/gen/traits/mod.rs +++ b/nutype_macros/src/any/gen/traits/mod.rs @@ -160,7 +160,7 @@ fn gen_implemented_traits( AnyIrregularTrait::Deref => Ok(gen_impl_trait_deref(type_name, generics, inner_type)), AnyIrregularTrait::Borrow => Ok(gen_impl_trait_borrow(type_name, generics, inner_type)), AnyIrregularTrait::FromStr => Ok( - gen_impl_trait_from_str(type_name, inner_type, maybe_error_type_name.as_ref()) + gen_impl_trait_from_str(type_name, generics, inner_type, maybe_error_type_name.as_ref()) ), AnyIrregularTrait::TryFrom => Ok( gen_impl_trait_try_from(type_name, generics, inner_type, maybe_error_type_name.as_ref()) diff --git a/nutype_macros/src/common/gen/parse_error.rs b/nutype_macros/src/common/gen/parse_error.rs index db8acf8..7cd3fe7 100644 --- a/nutype_macros/src/common/gen/parse_error.rs +++ b/nutype_macros/src/common/gen/parse_error.rs @@ -1,8 +1,12 @@ use cfg_if::cfg_if; use proc_macro2::TokenStream; use quote::{format_ident, quote}; +use syn::Generics; -use crate::common::models::{ErrorTypeName, InnerType, ParseErrorTypeName, TypeName}; +use crate::common::{ + gen::{add_bound_to_all_type_params, strip_trait_bounds_on_generics}, + models::{ErrorTypeName, InnerType, ParseErrorTypeName, TypeName}, +}; /// Generate a name for the error which is used for FromStr trait implementation. pub fn gen_parse_error_name(type_name: &TypeName) -> ParseErrorTypeName { @@ -13,15 +17,23 @@ pub fn gen_parse_error_name(type_name: &TypeName) -> ParseErrorTypeName { /// Generate an error which is used for FromStr trait implementation of non-string types (e.g. /// floats or integers) pub fn gen_def_parse_error( - inner_type: impl Into, type_name: &TypeName, + generics: &Generics, + inner_type: impl Into, maybe_error_type_name: Option<&ErrorTypeName>, parse_error_type_name: &ParseErrorTypeName, ) -> TokenStream { let inner_type: InnerType = inner_type.into(); let type_name_str = type_name.to_string(); + let generics_without_bounds = strip_trait_bounds_on_generics(generics); + let generics_with_fromstr_bound = add_bound_to_all_type_params( + &generics_without_bounds, + syn::parse_quote!(::core::str::FromStr), + ); + 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 { @@ -41,15 +53,15 @@ pub fn gen_def_parse_error( } } else { quote! { - #[derive(Debug)] - pub enum #parse_error_type_name { - Parse(<#inner_type as ::core::str::FromStr>::Err), - } + #[derive(Debug)] // #[derive(Debug) + pub enum #parse_error_type_name #generics_with_fromstr_bound { // pub enum ParseErrorFoo> { + Parse(<#inner_type as ::core::str::FromStr>::Err), // Parse(::Err), + } // } - 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), } } } @@ -58,8 +70,12 @@ pub fn gen_def_parse_error( cfg_if! { if #[cfg(feature = "std")] { + let generics_with_fromstr_and_debug_bounds = add_bound_to_all_type_params( + &generics_with_fromstr_bound, + syn::parse_quote!(::core::fmt::Debug), + ); let impl_std_error = quote! { - impl ::std::error::Error for #parse_error_type_name { + impl #generics_with_fromstr_and_debug_bounds ::std::error::Error for #parse_error_type_name #generics_without_bounds { fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> { None } diff --git a/nutype_macros/src/common/gen/traits.rs b/nutype_macros/src/common/gen/traits.rs index ac525be..98adbc6 100644 --- a/nutype_macros/src/common/gen/traits.rs +++ b/nutype_macros/src/common/gen/traits.rs @@ -203,25 +203,33 @@ pub fn gen_impl_trait_try_from( /// Generate implementation of FromStr trait for non-string types (e.g. integers or floats). pub fn gen_impl_trait_from_str( type_name: &TypeName, + generics: &Generics, inner_type: impl Into, maybe_error_type_name: Option<&ErrorTypeName>, ) -> TokenStream { let inner_type: InnerType = inner_type.into(); let parse_error_type_name = gen_parse_error_name(type_name); let def_parse_error = gen_def_parse_error( - inner_type.clone(), type_name, + generics, + inner_type.clone(), maybe_error_type_name, &parse_error_type_name, ); + let generics_without_bounds = strip_trait_bounds_on_generics(generics); + let generics_with_fromstr_bound = add_bound_to_all_type_params( + generics, + syn::parse_quote!(::core::str::FromStr), + ); + if let Some(_error_type_name) = maybe_error_type_name { // The case with validation quote! { #def_parse_error - impl ::core::str::FromStr for #type_name { - type Err = #parse_error_type_name; + impl #generics_with_fromstr_bound ::core::str::FromStr for #type_name #generics_without_bounds { + type Err = #parse_error_type_name #generics_without_bounds; fn from_str(raw_string: &str) -> ::core::result::Result { let raw_value: #inner_type = raw_string.parse().map_err(#parse_error_type_name::Parse)?; @@ -234,8 +242,8 @@ pub fn gen_impl_trait_from_str( quote! { #def_parse_error - impl ::core::str::FromStr for #type_name { - type Err = #parse_error_type_name; + impl #generics_with_fromstr_bound ::core::str::FromStr for #type_name #generics_without_bounds { + type Err = #parse_error_type_name #generics_without_bounds; fn from_str(raw_string: &str) -> ::core::result::Result { let value: #inner_type = raw_string.parse().map_err(#parse_error_type_name::Parse)?; diff --git a/nutype_macros/src/float/gen/traits/mod.rs b/nutype_macros/src/float/gen/traits/mod.rs index 4d21d77..47e37d9 100644 --- a/nutype_macros/src/float/gen/traits/mod.rs +++ b/nutype_macros/src/float/gen/traits/mod.rs @@ -172,7 +172,7 @@ fn gen_implemented_traits( FloatIrregularTrait::AsRef => Ok(gen_impl_trait_as_ref(type_name, generics, inner_type)), FloatIrregularTrait::Deref => Ok(gen_impl_trait_deref(type_name, generics, inner_type)), FloatIrregularTrait::FromStr => { - Ok(gen_impl_trait_from_str(type_name, inner_type, maybe_error_type_name.as_ref())) + Ok(gen_impl_trait_from_str(type_name, generics, inner_type, maybe_error_type_name.as_ref())) } FloatIrregularTrait::From => Ok(gen_impl_trait_from(type_name, generics, inner_type)), FloatIrregularTrait::Into => Ok(gen_impl_trait_into(type_name, generics, inner_type)), diff --git a/nutype_macros/src/integer/gen/traits/mod.rs b/nutype_macros/src/integer/gen/traits/mod.rs index 9dd265a..ebdffde 100644 --- a/nutype_macros/src/integer/gen/traits/mod.rs +++ b/nutype_macros/src/integer/gen/traits/mod.rs @@ -192,7 +192,7 @@ fn gen_implemented_traits( IntegerIrregularTrait::AsRef => Ok(gen_impl_trait_as_ref(type_name, generics, inner_type)), IntegerIrregularTrait::Deref => Ok(gen_impl_trait_deref(type_name, generics, inner_type)), IntegerIrregularTrait::FromStr => { - Ok(gen_impl_trait_from_str(type_name, inner_type, maybe_error_type_name.as_ref())) + Ok(gen_impl_trait_from_str(type_name, generics, inner_type, maybe_error_type_name.as_ref())) } IntegerIrregularTrait::From => Ok(gen_impl_trait_from(type_name, generics, inner_type)), IntegerIrregularTrait::Into => Ok(gen_impl_trait_into(type_name, generics, inner_type)), diff --git a/test_suite/tests/any.rs b/test_suite/tests/any.rs index 733c5a7..df3a4b5 100644 --- a/test_suite/tests/any.rs +++ b/test_suite/tests/any.rs @@ -179,7 +179,10 @@ mod traits { assert_eq!(loc, Location::new(Point::new(3, 5))); let err = "3,lol".parse::().unwrap_err(); - assert_eq!(err.to_string(), "Failed to parse Location: Invalid integer"); + assert_eq!( + err.to_string(), + "Failed to parse Location: \"Invalid integer\"" + ); } #[test] @@ -699,11 +702,33 @@ mod with_generics { #[test] fn test_generic_boundaries_from_str() { + #[nutype(derive(Debug, FromStr))] + struct Parseable(T); + + { + let xiii = "13".parse::>().unwrap(); + assert_eq!(xiii.into_inner(), 13); + } + + { + let vii = "vii".parse::>().unwrap(); + assert_eq!(vii.into_inner(), "vii"); + } + + { + let err = "iv".parse::>().unwrap_err(); + assert_eq!( + err.to_string(), + "Failed to parse Parseable: ParseIntError { kind: InvalidDigit }" + ); + } + } + + #[test] + fn test_generic_boundaries_from_str_with_lifetime() { // TODO - // #[nutype( - // derive(Debug, FromStr), - // )] - // struct Wrapper(T); + // #[nutype(derive(FromStr))] + // struct Clarabelle<'a>(Cow<'a, str>); } #[test] diff --git a/test_suite/tests/float.rs b/test_suite/tests/float.rs index 6ebc2bc..97df992 100644 --- a/test_suite/tests/float.rs +++ b/test_suite/tests/float.rs @@ -439,7 +439,7 @@ mod traits { let err: DistParseError = "foobar".parse::().unwrap_err(); assert_eq!( err.to_string(), - "Failed to parse Dist: invalid float literal" + "Failed to parse Dist: ParseFloatError { kind: Invalid }" ); } diff --git a/test_suite/tests/integer.rs b/test_suite/tests/integer.rs index 0052622..4f2612a 100644 --- a/test_suite/tests/integer.rs +++ b/test_suite/tests/integer.rs @@ -590,7 +590,7 @@ mod traits { let err: AgeParseError = "foobar".parse::().unwrap_err(); assert_eq!( err.to_string(), - "Failed to parse Age: invalid digit found in string" + "Failed to parse Age: ParseIntError { kind: InvalidDigit }" ); } From 16e9fe6f6075e96a706d7389d35cca7425350470 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Wed, 26 Jun 2024 07:44:17 +0200 Subject: [PATCH 2/2] Support derive(FromStr) for generics with validation --- Cargo.lock | 70 ++++++++++++++++++++- dummy/Cargo.toml | 1 + dummy/src/main.rs | 23 ++----- nutype_macros/src/common/gen/parse_error.rs | 15 +++-- test_suite/Cargo.toml | 1 + test_suite/tests/any.rs | 36 ++++++++--- test_suite/tests/float.rs | 2 +- test_suite/tests/integer.rs | 2 +- 8 files changed, 111 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8648df5..d934c65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,7 @@ version = "0.1.0" dependencies = [ "arbitrary", "lazy_static", + "num", "nutype", "once_cell", "regex", @@ -229,11 +230,75 @@ dependencies = [ "nutype", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -495,6 +560,7 @@ dependencies = [ "arbitrary", "arbtest 0.2.0", "lazy_static", + "num", "nutype", "once_cell", "regex", diff --git a/dummy/Cargo.toml b/dummy/Cargo.toml index 6599e15..281ec6b 100644 --- a/dummy/Cargo.toml +++ b/dummy/Cargo.toml @@ -15,3 +15,4 @@ once_cell = "*" lazy_static = "*" ron = "0.8.1" arbitrary = "1.3.2" +num = "0.4.3" diff --git a/dummy/src/main.rs b/dummy/src/main.rs index 694310d..2561bfd 100644 --- a/dummy/src/main.rs +++ b/dummy/src/main.rs @@ -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(Vec); +struct Even(T); -fn main() { - { - // Not empty vec is fine - let json = "[3, 1, 5, 2]"; - let sv = serde_json::from_str::>(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::>(json); - assert!(result.is_err()); - } -} +fn main() {} diff --git a/nutype_macros/src/common/gen/parse_error.rs b/nutype_macros/src/common/gen/parse_error.rs index 7cd3fe7..07a4db6 100644 --- a/nutype_macros/src/common/gen/parse_error.rs +++ b/nutype_macros/src/common/gen/parse_error.rs @@ -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> { + Parse(<#inner_type as ::core::str::FromStr>::Err), // Parse(::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), } diff --git a/test_suite/Cargo.toml b/test_suite/Cargo.toml index 60e19a9..7baf496 100644 --- a/test_suite/Cargo.toml +++ b/test_suite/Cargo.toml @@ -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"] diff --git a/test_suite/tests/any.rs b/test_suite/tests/any.rs index df3a4b5..7d43b9c 100644 --- a/test_suite/tests/any.rs +++ b/test_suite/tests/any.rs @@ -202,7 +202,7 @@ mod traits { let err = "6,5,4".parse::().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\"" ); } @@ -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 TryFrom for T @@ -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()) @@ -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); @@ -722,13 +722,33 @@ mod with_generics { "Failed to parse Parseable: ParseIntError { kind: InvalidDigit }" ); } + + { + let four = "4".parse::>>().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); + + { + let err = "13".parse::>().unwrap_err(); + assert_eq!( + err.to_string(), + "Failed to parse Even: Even failed the predicate test." + ); + } + + { + let twelve = "12".parse::>().unwrap(); + assert_eq!(twelve.into_inner(), 12); + } } #[test] diff --git a/test_suite/tests/float.rs b/test_suite/tests/float.rs index 97df992..1d21ff2 100644 --- a/test_suite/tests/float.rs +++ b/test_suite/tests/float.rs @@ -455,7 +455,7 @@ mod traits { let err: DistParseError = "foobar".parse::().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 diff --git a/test_suite/tests/integer.rs b/test_suite/tests/integer.rs index 4f2612a..ffa75b9 100644 --- a/test_suite/tests/integer.rs +++ b/test_suite/tests/integer.rs @@ -606,7 +606,7 @@ mod traits { let err: AgeParseError = "foobar".parse::().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