Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- **[FEATURE]** Ability to derive [`Valuable`](https://docs.rs/valuable/0.1.1/valuable/trait.Valuable.html) (requires `valuable` feature).
- **[FEATURE]** Ability to control constructor visibility with `constructor(visibility = ...)` attribute (see [#211](https://github.com/greyblake/nutype/issues/211)).
- **[FEATURE]** Add `len_utf16_min` and `len_utf16_max` validators for string types to validate UTF-16 code unit length (useful for JavaScript interop) (see [#162](https://github.com/greyblake/nutype/issues/162)).
- **[FEATURE]** Support `where` clauses in generic newtypes, including Higher-Ranked Trait Bounds (HRTB) like `for<'a> &'a C: IntoIterator` (see [#160](https://github.com/greyblake/nutype/issues/160)).

### v0.6.2 - 2025-06-30
- **[FEATURE]** Introduce `derive_unsafe(..)` attribute to derive any arbitrary trait (requires `derive_unsafe` feature to be enabled).
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,32 @@ assert_eq!(numbers.as_ref(), &[1, 2, 4, 7]);
assert_eq!(numbers.len(), 4);
```

### Where clauses

Nutype fully supports `where` clauses in generic newtypes, including Higher-Ranked Trait Bounds (HRTB):

```rust
use nutype::nutype;

// Simple where clause
#[nutype(derive(Debug, Clone))]
struct Wrapper<T>(T)
where
T: Default + Clone;

// HRTB for collections - validate that collection is non-empty
#[nutype(
validate(predicate = |c| c.into_iter().next().is_some()),
derive(Debug)
)]
struct NonEmpty<C>(C)
where
for<'a> &'a C: IntoIterator;

// Usage
let non_empty = NonEmpty::try_new(vec![1, 2, 3]).unwrap();
assert!(NonEmpty::try_new(Vec::<i32>::new()).is_err());
```

## Custom sanitizers

Expand Down
27 changes: 27 additions & 0 deletions nutype/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,33 @@
//! assert_eq!(numbers.len(), 4);
//! ```
//!
//! ### Where clauses
//!
//! Nutype fully supports `where` clauses in generic newtypes, including Higher-Ranked Trait Bounds (HRTB):
//!
//! ```
//! use nutype::nutype;
//!
//! // Simple where clause
//! #[nutype(derive(Debug, Clone))]
//! struct Wrapper<T>(T)
//! where
//! T: Default + Clone;
//!
//! // HRTB for collections - validate that collection is non-empty
//! #[nutype(
//! validate(predicate = |c| c.into_iter().next().is_some()),
//! derive(Debug)
//! )]
//! struct NonEmpty<C>(C)
//! where
//! for<'a> &'a C: IntoIterator;
//!
//! // Usage
//! let non_empty = NonEmpty::try_new(vec![1, 2, 3]).unwrap();
//! assert!(NonEmpty::try_new(Vec::<i32>::new()).is_err());
//! ```
//!
//! ## Custom sanitizers
//!
//! You can set custom sanitizers using the `with` option.
Expand Down
28 changes: 23 additions & 5 deletions nutype_macros/src/any/generate/traits/arbitrary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use syn::Generics;

use crate::{
any::models::{AnyGuard, AnyInnerType},
common::generate::{add_bound_to_all_type_params, add_param, strip_trait_bounds_on_generics},
common::generate::generics::{SplitGenerics, add_bound_to_all_type_params, add_generic_param},
common::models::TypeName,
};

Expand All @@ -25,14 +25,32 @@ pub fn gen_impl_trait_arbitrary(

// Generate implementation of `Arbitrary` trait, assuming that inner type implements Arbitrary
// too.
let generics_without_bounds = strip_trait_bounds_on_generics(generics);
let generics_with_lifetime = add_param(&generics_without_bounds, quote!('nu_arb));
//
// We need to:
// 1. Add a lifetime 'nu_arb
// 2. Add Arbitrary<'nu_arb> bound to all type params
let generics_with_lifetime = add_generic_param(generics, syn::parse_quote!('nu_arb));
let generics_with_bounds = add_bound_to_all_type_params(
&generics_with_lifetime,
quote!(::arbitrary::Arbitrary<'nu_arb>),
syn::parse_quote!(::arbitrary::Arbitrary<'nu_arb>),
);

let SplitGenerics {
impl_generics,
type_generics: _,
where_clause,
} = SplitGenerics::new(&generics_with_bounds);

// Get type generics without the added lifetime
let SplitGenerics { type_generics, .. } = SplitGenerics::new(generics);

// Example for `struct Wrapper<T>(T) where T: Clone`:
//
// impl<'nu_arb, T: Arbitrary<'nu_arb>> Arbitrary<'nu_arb> for Wrapper<T> where T: Clone {
// fn arbitrary(u: &mut Unstructured<'nu_arb>) -> Result<Self> { ... }
// }
Ok(quote!(
impl #generics_with_bounds ::arbitrary::Arbitrary<'nu_arb> for #type_name #generics_without_bounds {
impl #impl_generics ::arbitrary::Arbitrary<'nu_arb> for #type_name #type_generics #where_clause {
fn arbitrary(u: &mut ::arbitrary::Unstructured<'nu_arb>) -> ::arbitrary::Result<Self> {
let inner_value: #inner_type = u.arbitrary()?;
Ok(#type_name::new(inner_value))
Expand Down
65 changes: 45 additions & 20 deletions nutype_macros/src/any/generate/traits/into_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use syn::Generics;
use crate::{
any::models::AnyInnerType,
common::{
generate::{add_param, strip_trait_bounds_on_generics},
generate::generics::{SplitGenerics, add_generic_param},
models::TypeName,
},
};
Expand All @@ -15,36 +15,61 @@ pub fn gen_impl_trait_into_iter(
generics: &Generics,
inner_type: &AnyInnerType,
) -> TokenStream {
let generics_without_bounds = strip_trait_bounds_on_generics(generics);
let generics_with_iter_lifetime = add_param(generics, quote!('__nutype_iter));
let generics_with_iter_lifetime =
add_generic_param(generics, syn::parse_quote!('__nutype_iter));

let SplitGenerics {
impl_generics,
type_generics,
where_clause,
} = SplitGenerics::new(generics);

let SplitGenerics {
impl_generics: impl_generics_with_lifetime,
type_generics: _,
where_clause: where_clause_with_lifetime,
} = SplitGenerics::new(&generics_with_iter_lifetime);

// In the comments below, we assume that IntoIterator is derived for the following type
//
// struct Names<'a, T: Display>(Vec<&'a T>);
// struct Names<'a, T: Display>(Vec<&'a T>) where T: Clone;
//
// NOTE: We deliberately do not generate an iterator over mutable references, because
// this would allow the user to modify the elements of the collection, which may violate
// the guarantees that nutype is supposed to provide.
//
// Example generated code:
//
// impl<'a, T: Display> IntoIterator for Names<'a, T> where T: Clone {
// type Item = <Vec<&'a T> as IntoIterator>::Item;
// type IntoIter = <Vec<&'a T> as IntoIterator>::IntoIter;
// fn into_iter(self) -> Self::IntoIter { self.0.into_iter() }
// }
//
// impl<'a, '__nutype_iter, T: Display> IntoIterator for &'__nutype_iter Names<'a, T> where T: Clone {
// type Item = <&'__nutype_iter Vec<&'a T> as IntoIterator>::Item;
// ...
// }
quote!(
// Implement IntoIterator for the type.
impl #generics ::core::iter::IntoIterator for #type_name #generics_without_bounds { // impl<'a, T: Display> ::core::iter::IntoIterator for Names<'a, T> {
type Item = <#inner_type as ::core::iter::IntoIterator>::Item; // type Item = <Vec<&'a T> as ::core::iter::IntoIterator>::Item;
type IntoIter = <#inner_type as ::core::iter::IntoIterator>::IntoIter; // type IntoIter = <Vec<&'a T> as ::core::iter::IntoIterator>::IntoIter;
//
fn into_iter(self) -> Self::IntoIter { // fn into_iter(self) -> Self::IntoIter {
self.0.into_iter() // self.0.into_iter()
} // }
} // }
impl #impl_generics ::core::iter::IntoIterator for #type_name #type_generics #where_clause {
type Item = <#inner_type as ::core::iter::IntoIterator>::Item;
type IntoIter = <#inner_type as ::core::iter::IntoIterator>::IntoIter;

fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

// IntoIterator for the reference to the type (so it can be iterated over references).
impl #generics_with_iter_lifetime ::core::iter::IntoIterator // impl<'a, '__nutype_iter, T: Display> ::core::iter::IntoIterator
for &'__nutype_iter #type_name #generics_without_bounds { // for &'__nutype_iter Names<'a, T> {
type Item = <&'__nutype_iter #inner_type as ::core::iter::IntoIterator>::Item; // type Item = <&'__nutype_iter Vec<&'a T> as ::core::iter::IntoIterator>::Item;
type IntoIter = <&'__nutype_iter #inner_type as ::core::iter::IntoIterator>::IntoIter; // type IntoIter = <&'__nutype_iter Vec<&'a T> as ::core::iter::IntoIterator>::IntoIter;

fn into_iter(self) -> Self::IntoIter { // fn into_iter(self) -> Self::IntoIter {
self.0.iter().into_iter() // self.0.iter().into_iter()
} // }
impl #impl_generics_with_lifetime ::core::iter::IntoIterator
for &'__nutype_iter #type_name #type_generics #where_clause_with_lifetime {
type Item = <&'__nutype_iter #inner_type as ::core::iter::IntoIterator>::Item;
type IntoIter = <&'__nutype_iter #inner_type as ::core::iter::IntoIterator>::IntoIter;

fn into_iter(self) -> Self::IntoIter {
self.0.iter().into_iter()
}
}
)
}
143 changes: 143 additions & 0 deletions nutype_macros/src/common/generate/generics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//! Utilities for handling generics and where clauses in code generation.
//!
//! This module provides helper functions that properly handle `where` clauses
//! when generating impl blocks, including support for Higher-Ranked Trait Bounds (HRTB).

use proc_macro2::TokenStream;
use quote::quote;
use syn::Generics;

/// Split generics for use in impl blocks.
///
/// This properly separates the generics into three parts:
/// - `impl_generics`: Goes after `impl` keyword (e.g., `<T: Clone>`)
/// - `type_generics`: Goes after type name (e.g., `<T>`)
/// - `where_clause`: Goes at the end of impl signature (e.g., `where T: Default`)
///
/// # Example
///
/// For `struct Foo<T: Clone>(T) where T: Default`:
///
/// ```ignore
/// let split = split_generics_for_impl(&generics);
/// quote! {
/// impl #impl_generics SomeTrait for Foo #type_generics #where_clause {
/// // ...
/// }
/// }
/// ```
///
/// Generates:
/// ```ignore
/// impl<T: Clone> SomeTrait for Foo<T> where T: Default {
/// // ...
/// }
/// ```
pub struct SplitGenerics {
pub impl_generics: TokenStream,
pub type_generics: TokenStream,
pub where_clause: TokenStream,
}

impl SplitGenerics {
pub fn new(generics: &Generics) -> Self {
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
Self {
impl_generics: quote!(#impl_generics),
type_generics: quote!(#type_generics),
where_clause: quote!(#where_clause),
}
}
}

/// Add a bound to all type parameters in generics.
///
/// This adds the bound to inline type parameters.
///
/// # Arguments
/// * `generics` - The original generics
/// * `bound` - The bound to add (e.g., `Display`, `Serialize`)
///
/// # Example
///
/// Input: `<T, U>` with bound `Display`
/// Output: `<T: Display, U: Display>`
pub fn add_bound_to_all_type_params(generics: &Generics, bound: syn::TypeParamBound) -> Generics {
let mut result = generics.clone();
for param in &mut result.params {
if let syn::GenericParam::Type(type_param) = param {
type_param.bounds.push(bound.clone());
}
}
result
}

/// Add a generic parameter (typically a lifetime) to generics.
///
/// The parameter is added at the end of the params list.
///
/// # Example
///
/// Input: `<T, U>` with param `'de`
/// Output: `<T, U, 'de>`
pub fn add_generic_param(generics: &Generics, param: syn::GenericParam) -> Generics {
let mut result = generics.clone();
result.params.push(param);
result
}

#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;

#[test]
fn test_split_generics_simple() {
let generics: Generics = parse_quote!(<T: Clone>);
let split = SplitGenerics::new(&generics);

// Just verify it doesn't panic and produces some output
assert!(!split.impl_generics.is_empty());
assert!(!split.type_generics.is_empty());
}

#[test]
fn test_split_generics_with_where_clause() {
// Parse a full struct to get generics with where clause
let item: syn::ItemStruct = parse_quote! {
struct Foo<T> where T: Clone { field: T }
};
let split = SplitGenerics::new(&item.generics);

// Verify where clause is captured
assert!(!split.where_clause.is_empty());
}

#[test]
fn test_split_generics_with_hrtb() {
// Parse a full struct to get generics with HRTB where clause
let item: syn::ItemStruct = parse_quote! {
struct Foo<C> where for<'a> &'a C: IntoIterator { field: C }
};
let split = SplitGenerics::new(&item.generics);

// Verify HRTB where clause is captured
let where_str = split.where_clause.to_string();
assert!(where_str.contains("for"));
assert!(where_str.contains("IntoIterator"));
}

#[test]
fn test_add_bound() {
let generics: Generics = parse_quote!(<T, U>);
let bound: syn::TypeParamBound = parse_quote!(Clone);
let result = add_bound_to_all_type_params(&generics, bound);

// Verify bounds were added
for param in &result.params {
if let syn::GenericParam::Type(tp) = param {
assert!(!tp.bounds.is_empty());
}
}
}
}
Loading
Loading