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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
- **[FEATURE]** Friendlier error when a `#[nutype(...)]` attribute is mistyped: suggests the closest match (e.g. `validte` -> `validate`) and lists the available nutype attributes (see [#240](https://github.com/greyblake/nutype/issues/240)).
- **[FIX]** Fix misleading error for value-type mismatches in validators (see [#241](https://github.com/greyblake/nutype/issues/241)).
- **[FIX]** Improve rust-analyzer resilience: when `#[nutype(...)]` arguments fail to parse (e.g. while still being typed), emit a best-effort type skeleton alongside the error so the newtype stays resolvable and downstream completions keep working (see [#178](https://github.com/greyblake/nutype/issues/178)).
- **[FIX]** Correct the validation error `Display` message for float newtypes: `less` now reads "The value must be less than ..." and `less_or_equal` reads "The value must be less or equal to ..." (the two were previously swapped). Integer and decimal were already correct.
- **[INTERNAL]** Consolidate the integer, float and decimal backends onto a shared numeric code-generation and validation layer (`common/generate/numeric.rs` and shared helpers in `common/validate.rs`), removing a large amount of duplicated code. No change to generated code or public API.

### v0.7.0 - 2026-04-25
- **[BREAKING]** Rename `derive_unsafe` to `derive_unchecked` (both the feature flag and the attribute).
Expand Down
1 change: 1 addition & 0 deletions nutype_macros/src/common/generate/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod error;
pub mod generics;
pub mod new_unchecked;
pub mod numeric;
pub mod parse_error;
pub mod tests;
pub mod traits;
Expand Down
234 changes: 234 additions & 0 deletions nutype_macros/src/common/generate/numeric.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
//! Shared code generation for numeric inner types (integer, float, decimal).
//!
//! Integer, float and decimal newtypes share almost all of their generated
//! code: the `__sanitize__` function, the `__validate__` function (bound checks
//! and predicate), and the validation error enum with its `Display` impl. The
//! only real differences are:
//! * float additionally supports the `Finite` validator;
//! * each kind allows a slightly different set of derivable traits (handled in
//! the per-kind `validate.rs`, not here).
//!
//! To avoid maintaining three near-identical copies, each kind implements
//! [`NumericValidatorTokens`] and [`NumericSanitizerTokens`] for its validator
//! and sanitizer enums, and the generation below is written once against those
//! traits.

use proc_macro2::TokenStream;
use quote::{ToTokens, quote};

use crate::common::{
generate::error::gen_impl_error_trait,
models::{ConstFn, ErrorTypePath, TypeName},
};

/// A normalized, kind-agnostic view of a single numeric validator.
///
/// Each numeric validator enum (`IntegerValidator`, `FloatValidator`,
/// `DecimalValidator`) maps onto these variants. The associated value of a
/// bound (or the predicate function) is exposed as `&dyn ToTokens`, which is all
/// the code generation needs.
pub enum NumericValidatorView<'a> {
Greater(&'a dyn ToTokens),
GreaterOrEqual(&'a dyn ToTokens),
Less(&'a dyn ToTokens),
LessOrEqual(&'a dyn ToTokens),
Predicate(&'a dyn ToTokens),
Finite,
}

/// Implemented by every numeric validator enum so the shared generators can
/// treat them uniformly.
pub trait NumericValidatorTokens {
fn view(&self) -> NumericValidatorView<'_>;
}

/// Implemented by every numeric sanitizer enum. Numeric sanitizers only ever
/// carry a custom `with = ...` function (plus a `_Phantom` variant that is
/// never constructed), so a single accessor is enough.
pub trait NumericSanitizerTokens {
/// Returns the custom sanitizer function, or `None` for the phantom variant.
fn custom_fn(&self) -> Option<&dyn ToTokens>;
}

/// Generate the `__sanitize__` function shared by all numeric kinds.
pub fn gen_numeric_fn_sanitize<S, IT>(
inner_type: &IT,
sanitizers: &[S],
const_fn: ConstFn,
) -> TokenStream
where
S: NumericSanitizerTokens,
IT: ToTokens,
{
let transformations: TokenStream = sanitizers
.iter()
.filter_map(|san| san.custom_fn())
.map(|custom_sanitizer| {
quote!(
value = (#custom_sanitizer)(value);
)
})
.collect();

quote!(
#const_fn fn __sanitize__(mut value: #inner_type) -> #inner_type {
#transformations
value
}
)
}

/// Generate the `__validate__` function shared by all numeric kinds.
pub fn gen_numeric_fn_validate<V, IT>(
inner_type: &IT,
error_type_path: &ErrorTypePath,
validators: &[V],
const_fn: ConstFn,
) -> TokenStream
where
V: NumericValidatorTokens,
IT: ToTokens,
{
let validations: TokenStream = validators
.iter()
.map(|validator| match validator.view() {
NumericValidatorView::Less(exclusive_upper_bound) => {
quote!(
if val >= #exclusive_upper_bound {
return Err(#error_type_path::LessViolated);
}
)
}
NumericValidatorView::LessOrEqual(max) => {
quote!(
if val > #max {
return Err(#error_type_path::LessOrEqualViolated);
}
)
}
NumericValidatorView::Greater(exclusive_lower_bound) => {
quote!(
if val <= #exclusive_lower_bound {
return Err(#error_type_path::GreaterViolated);
}
)
}
NumericValidatorView::GreaterOrEqual(min) => {
quote!(
if val < #min {
return Err(#error_type_path::GreaterOrEqualViolated);
}
)
}
NumericValidatorView::Predicate(custom_is_valid_fn) => {
quote!(
if !(#custom_is_valid_fn)(&val) {
return Err(#error_type_path::PredicateViolated);
}
)
}
NumericValidatorView::Finite => {
quote!(
if !val.is_finite() {
return Err(#error_type_path::FiniteViolated);
}
)
}
})
.collect();

quote!(
#const_fn fn __validate__(val: &#inner_type) -> ::core::result::Result<(), #error_type_path> {
let val = *val;
#validations
Ok(())
}
)
}

/// Generate the validation error enum (definition + `Display` + `Error`) shared
/// by all numeric kinds.
pub fn gen_numeric_validation_error_type<V>(
type_name: &TypeName,
error_type_path: &ErrorTypePath,
validators: &[V],
) -> TokenStream
where
V: NumericValidatorTokens,
{
let definition = gen_definition(error_type_path, validators);
let impl_display_trait = gen_impl_display_trait(type_name, error_type_path, validators);
let impl_error_trait = gen_impl_error_trait(error_type_path);

quote! {
#[derive(Debug, Clone, PartialEq, Eq)]
#definition

#impl_display_trait
#impl_error_trait
}
}

fn gen_definition<V>(error_type_path: &ErrorTypePath, validators: &[V]) -> TokenStream
where
V: NumericValidatorTokens,
{
let error_variants: TokenStream = validators
.iter()
.map(|validator| match validator.view() {
NumericValidatorView::Greater(_) => quote!(GreaterViolated,),
NumericValidatorView::GreaterOrEqual(_) => quote!(GreaterOrEqualViolated,),
NumericValidatorView::Less(_) => quote!(LessViolated,),
NumericValidatorView::LessOrEqual(_) => quote!(LessOrEqualViolated,),
NumericValidatorView::Predicate(_) => quote!(PredicateViolated,),
NumericValidatorView::Finite => quote!(FiniteViolated,),
})
.collect();

quote! {
#[allow(clippy::enum_variant_names)]
pub enum #error_type_path {
#error_variants
}
}
}

fn gen_impl_display_trait<V>(
type_name: &TypeName,
error_type_path: &ErrorTypePath,
validators: &[V],
) -> TokenStream
where
V: NumericValidatorTokens,
{
let match_arms = validators.iter().map(|validator| match validator.view() {
NumericValidatorView::Greater(val) => quote! {
#error_type_path::GreaterViolated => write!(f, "{} is too small. The value must be greater than {:#?}.", stringify!(#type_name), #val)
},
NumericValidatorView::GreaterOrEqual(val) => quote! {
#error_type_path::GreaterOrEqualViolated => write!(f, "{} is too small. The value must be greater or equal to {:#?}.", stringify!(#type_name), #val)
},
NumericValidatorView::Less(val) => quote! {
#error_type_path::LessViolated => write!(f, "{} is too big. The value must be less than {:#?}.", stringify!(#type_name), #val)
},
NumericValidatorView::LessOrEqual(val) => quote! {
#error_type_path::LessOrEqualViolated => write!(f, "{} is too big. The value must be less or equal to {:#?}.", stringify!(#type_name), #val)
},
NumericValidatorView::Predicate(_) => quote! {
#error_type_path::PredicateViolated => write!(f, "{} failed the predicate test.", stringify!(#type_name))
},
NumericValidatorView::Finite => quote! {
#error_type_path::FiniteViolated => write!(f, "{} is not finite.", stringify!(#type_name))
},
});

quote! {
impl ::core::fmt::Display for #error_type_path {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
match self {
#(#match_arms,)*
}
}
}
}
}
58 changes: 51 additions & 7 deletions nutype_macros/src/common/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -809,17 +809,21 @@ impl ToTokens for TypedCustomFunction {
}
}

/// This trait allows to reuse validation of numeric validators.
pub trait NumericBoundValidator<T: Clone> {
fn greater(&self) -> Option<T>;
fn greater_or_equal(&self) -> Option<T>;
fn less(&self) -> Option<T>;
fn less_or_equal(&self) -> Option<T>;
/// This trait allows to reuse validation of numeric validators. The associated
/// `Bound` type is the inner value type of the bounds (e.g. `i32` for an integer
/// newtype), which is a function of the validator type itself.
pub trait NumericBoundValidator {
type Bound: Clone;
fn greater(&self) -> Option<Self::Bound>;
fn greater_or_equal(&self) -> Option<Self::Bound>;
fn less(&self) -> Option<Self::Bound>;
fn less_or_equal(&self) -> Option<Self::Bound>;
}

macro_rules! impl_numeric_bound_validator {
($tp:ident) => {
impl<T: Clone> crate::common::models::NumericBoundValidator<T> for $tp<T> {
impl<T: Clone> crate::common::models::NumericBoundValidator for $tp<T> {
type Bound = T;
fn greater(&self) -> Option<T> {
if let $tp::Greater(ValueOrExpr::Value(value)) = self {
Some(value.clone())
Expand Down Expand Up @@ -919,3 +923,43 @@ macro_rules! impl_numeric_bound_on_vec_of {
}

pub(crate) use impl_numeric_bound_on_vec_of;

/// Define the inner-type enum for a numeric kind (integer, float) together with
/// its marker-trait impls, `ToTokens` and `Display`. Integer and float share
/// the exact same shape, differing only in the enum name, the marker trait and
/// the list of concrete types.
macro_rules! define_numeric_inner_type {
($enum_name:ident, $marker_trait:ident, $($tp:ty => $variant:ident),* $(,)?) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum $enum_name {
$($variant),*
}

$(
impl $marker_trait for $tp {}
)*

impl ::quote::ToTokens for $enum_name {
fn to_tokens(&self, token_stream: &mut ::proc_macro2::TokenStream) {
let type_stream = match self {
$(
Self::$variant => ::quote::quote!($tp),
)*
};
::quote::ToTokens::to_tokens(&type_stream, token_stream);
}
}

impl ::core::fmt::Display for $enum_name {
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
$(
Self::$variant => stringify!($tp).fmt(f),
)*
}
}
}
};
}

pub(crate) use define_numeric_inner_type;
Loading
Loading