Skip to content

Commit

Permalink
Merge pull request #149 from greyblake/generics-derive-from-str
Browse files Browse the repository at this point in the history
Start implementing support of derive(FromStr) for generic newtypes
  • Loading branch information
greyblake authored Jun 26, 2024
2 parents 91afd23 + 16e9fe6 commit 7c423f7
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 59 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() {}
2 changes: 1 addition & 1 deletion nutype_macros/src/any/gen/traits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
47 changes: 31 additions & 16 deletions nutype_macros/src/common/gen/parse_error.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -13,26 +17,33 @@ 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<InnerType>,
type_name: &TypeName,
generics: &Generics,
inner_type: impl Into<InnerType>,
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<Err: ::core::fmt::Debug>),
);

let definition = if let Some(error_type_name) = maybe_error_type_name {
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 All @@ -41,15 +52,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<T: ::core::str::FromStr<Err: ::core::fmt::Debug>> {
Parse(<#inner_type as ::core::str::FromStr>::Err), // Parse(<Foo as ::core::str::FromStr>::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),
}
}
}
Expand All @@ -58,8 +69,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
}
Expand Down
18 changes: 13 additions & 5 deletions nutype_macros/src/common/gen/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InnerType>,
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<Err: ::core::fmt::Debug>),
);

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<Self, Self::Err> {
let raw_value: #inner_type = raw_string.parse().map_err(#parse_error_type_name::Parse)?;
Expand All @@ -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<Self, Self::Err> {
let value: #inner_type = raw_string.parse().map_err(#parse_error_type_name::Parse)?;
Expand Down
2 changes: 1 addition & 1 deletion nutype_macros/src/float/gen/traits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ fn gen_implemented_traits<T: ToTokens>(
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)),
Expand Down
2 changes: 1 addition & 1 deletion nutype_macros/src/integer/gen/traits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ fn gen_implemented_traits<T: ToTokens>(
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)),
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
Loading

0 comments on commit 7c423f7

Please sign in to comment.