Skip to content

Commit

Permalink
Make derive(Deserialize) work
Browse files Browse the repository at this point in the history
  • Loading branch information
greyblake committed Jun 22, 2024
1 parent 3336939 commit a65148a
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 24 deletions.
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,6 @@ assert_eq!(name.into_inner(), " boo ");
* IDEs may not be very helpful at giving you hints about proc macros.
* Design of nutype may enforce you to run unnecessary validation (e.g. on loading data from DB), which may have a negative impact if you aim for extreme performance.

## A note about #[derive(...)]

You've got to know that the `#[nutype]` macro intercepts `#[derive(...)]` macro.
It's done on purpose to ensure that anything like `DerefMut` or `BorrowMut`, that can lead to a violation of the validation rules is excluded.
The library takes a conservative approach and it has its downside: deriving traits that are not known to the library is not possible.

## Support Ukrainian military forces

Today I live in Berlin, I have the luxury to live a physically safe life.
Expand Down
19 changes: 15 additions & 4 deletions dummy/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@ use std::cmp::Ord;

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

fn main() {
let v = SortedVec::new(vec![10, 3, 5, 2, 4]);
assert_eq!(v.into_inner(), vec![2, 3, 4, 5, 10]);
{
// 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());
}
}
42 changes: 41 additions & 1 deletion examples/any_generics/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use nutype::nutype;
use std::borrow::Cow;
use std::cmp::Ord;

/// A wrapper around a vector that is guaranteed to be sorted.
#[nutype(
sanitize(with = |mut v| { v.sort(); v }),
derive(Debug)
derive(Debug, Deserialize, Serialize)
)]
struct SortedVec<T: Ord>(Vec<T>);

Expand All @@ -15,6 +16,13 @@ struct SortedVec<T: Ord>(Vec<T>);
)]
struct NotEmpty<T>(Vec<T>);

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

/// An example with lifetimes
#[nutype(derive(
Debug,
Expand All @@ -41,10 +49,25 @@ struct NotEmpty<T>(Vec<T>);
struct Clarabelle<'a>(Cow<'a, str>);

fn main() {
// SortedVec
//
{
let v = SortedVec::new(vec![3, 0, 2]);
assert_eq!(v.into_inner(), vec![0, 2, 3]);
}
{
let sv = SortedVec::new(vec![4i32, 2, 8, 5]);
let json = serde_json::to_string(&sv).unwrap();
assert_eq!(json, "[2,4,5,8]");
}
{
let json = "[5,3,7]";
let sv = serde_json::from_str::<SortedVec<i32>>(json).unwrap();
assert_eq!(sv.into_inner(), vec![3, 5, 7]);
}

// NotEmpty
//
{
let v = NotEmpty::new(vec![1, 2, 3]).unwrap();
assert_eq!(v.into_inner(), vec![1, 2, 3]);
Expand All @@ -53,6 +76,23 @@ fn main() {
assert_eq!(err, NotEmptyError::PredicateViolated);
}

// SortedNotEmptyVec
//
{
// 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());
}

// Clarabelle (Cow)
//
{
let muu = Clarabelle::new(Cow::Borrowed("Muu"));
assert_eq!(muu.to_string(), "Muu");
Expand Down
20 changes: 20 additions & 0 deletions nutype_macros/src/common/gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,26 @@ fn strip_trait_bounds_on_generics(original: &Generics) -> Generics {
generics
}

/// Add a bound to all generics types.
///
/// Input:
/// <T, U>
/// Serialize
///
/// Output:
/// <T: Serialize, U: Serialize>
fn add_bound_to_all_type_params(generics: &Generics, bound: TokenStream) -> Generics {
let mut generics = generics.clone();
let parsed_bound: syn::TypeParamBound =
syn::parse2(bound).expect("Failed to parse TypeParamBound");
for param in &mut generics.params {
if let syn::GenericParam::Type(syn::TypeParam { bounds, .. }) = param {
bounds.push(parsed_bound.clone());
}
}
generics
}

pub trait GenerateNewtype {
type Sanitizer;
type Validator;
Expand Down
24 changes: 18 additions & 6 deletions nutype_macros/src/common/gen/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use quote::{quote, ToTokens};
use syn::Generics;

use crate::common::{
gen::strip_trait_bounds_on_generics,
gen::{add_bound_to_all_type_params, strip_trait_bounds_on_generics},
models::{ErrorTypeName, InnerType, TypeName},
};

Expand Down Expand Up @@ -243,9 +243,15 @@ pub fn gen_impl_trait_from_str(
}

pub fn gen_impl_trait_serde_serialize(type_name: &TypeName, generics: &Generics) -> TokenStream {
let generics_without_bounds = strip_trait_bounds_on_generics(generics);

// Turn `<T>` into `<T: Serialize>`
let all_generics_with_serialize_bound =
add_bound_to_all_type_params(&generics, syn::parse_quote!(::serde::Serialize));

let type_name_str = type_name.to_string();
quote! {
impl #generics ::serde::Serialize for #type_name #generics {
impl #all_generics_with_serialize_bound ::serde::Serialize for #type_name #generics_without_bounds {
fn serialize<S>(&self, serializer: S) -> ::core::result::Result<S::Ok, S::Error>
where
S: ::serde::Serializer
Expand Down Expand Up @@ -287,17 +293,23 @@ pub fn gen_impl_trait_serde_deserialize(
all_generics.params.push(syn::parse_quote!('de));
all_generics
};
let all_generics_without_bounds = strip_trait_bounds_on_generics(&all_generics);
let type_generics_without_bounds = strip_trait_bounds_on_generics(&type_generics);

// Turn `<'de, T>` into `<'de, T: Deserialize<'de>>`
let all_generics_with_deserialize_bound =
add_bound_to_all_type_params(&all_generics, syn::parse_quote!(::serde::Deserialize<'de>));

quote! {
impl #all_generics ::serde::Deserialize<'de> for #type_name #type_generics {
impl #all_generics_with_deserialize_bound ::serde::Deserialize<'de> for #type_name #type_generics_without_bounds {
fn deserialize<D: ::serde::Deserializer<'de>>(deserializer: D) -> ::core::result::Result<Self, D::Error> {
struct __Visitor #all_generics {
marker: ::std::marker::PhantomData<#type_name #type_generics>,
marker: ::std::marker::PhantomData<#type_name #type_generics_without_bounds>,
lifetime: ::std::marker::PhantomData<&'de ()>,
}

impl #all_generics ::serde::de::Visitor<'de> for __Visitor #all_generics {
type Value = #type_name #type_generics;
impl #all_generics_with_deserialize_bound ::serde::de::Visitor<'de> for __Visitor #all_generics_without_bounds {
type Value = #type_name #type_generics_without_bounds;

fn expecting(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
write!(formatter, #expecting_str)
Expand Down
30 changes: 23 additions & 7 deletions test_suite/tests/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,16 +637,32 @@ mod with_generics {
assert_eq!(set.len(), 2);
}

#[test]
fn test_generic_boundaries_serialize() {
// TODO
}
#[cfg(feature = "serde")]
mod serialization_with_generics {
use super::*;
use serde::{Deserialize, Serialize};

#[test]
fn test_generic_boundaries_deserialize() {
// TODO
#[test]
fn test_serialize() {
#[nutype(derive(Debug, Serialize))]
struct Wrapper<T: Serialize>(T);

let w = Wrapper::new(13);
let json = serde_json::to_string(&w).unwrap();
assert_eq!(json, "13");
}
}

// #[test]
// fn test_generic_boundaries_serialize() {
// // TODO
// }

// #[test]
// fn test_generic_boundaries_deserialize() {
// // TODO
// }

#[test]
fn test_generic_boundaries_from_str() {
// TODO
Expand Down

0 comments on commit a65148a

Please sign in to comment.