Skip to content

Releases: greyblake/nutype

Nutype 0.7.0 - conditional derives and more

25 Apr 12:11
e951afd

Choose a tag to compare

What is nutype?

Nutype is a Rust proc macro for type-driven domain modeling: it turns the newtype pattern into refined, branded types with built-in sanitizers (trim, lowercase, custom) and validators (length, range, regex, predicates, custom errors). It's "parse, don't validate" applied to your domain, useful for DDD, schema validation at API boundaries, and making illegal states unrepresentable, even across serde, FromStr, and arbitrary fuzzing.

Changes in v0.7.0

  • [BREAKING] Renamed derive_unsafe to derive_unchecked (both the feature flag and the attribute).
  • [FEATURE] Support cfg_attr for conditional derives (see #188).
  • [FEATURE] Support where clauses in generic newtypes, including Higher-Ranked Trait Bounds (HRTB) (see #160).
  • [FEATURE] Ability to control constructor visibility with the constructor(visibility = ...) attribute (see #211).
  • [FEATURE] Add len_utf16_min and len_utf16_max validators for string types (see #162).
  • [FEATURE] Ability to derive Valuable (requires the valuable feature).

Conditional derives via cfg_attr

A long-standing request (#188): derive traits only when a feature is enabled, only in tests, or under any other cfg predicate.

In v0.7.0 you can now write cfg_attr(...) directly inside #[nutype(...)], mirroring the standard #[cfg_attr] syntax:

use nutype::nutype;

#[nutype(
    sanitize(trim, lowercase),
    validate(not_empty, len_char_max = 100),
    derive(Debug, Clone, PartialEq, AsRef),
    cfg_attr(feature = "serde", derive(Serialize, Deserialize))
)]
pub struct Email(String);

Here Serialize and Deserialize are only derived when the serde feature is active. Note: nutype/serde must still be enabled at compile time so the macro recognizes those traits, but the actual derive is gated by cfg_attr.

You can use any predicate the standard cfg_attr accepts, including all(...), any(...), and not(...), and you can stack multiple cfg_attr(..) entries:

#[nutype(
    validate(not_empty),
    derive(Debug),
    cfg_attr(test, derive(Clone)),
    cfg_attr(feature = "serde", derive(Serialize, Deserialize))
)]
pub struct Tag(String);

A complete walkthrough lives in examples/cfg_attr_example.

where clauses and HRTB for generic newtypes

Generic newtypes now accept full where clauses, including Higher-Ranked Trait Bounds (#160):

use nutype::nutype;

#[nutype(
    validate(predicate = |c| c.into_iter().next().is_some()),
    derive(Debug)
)]
struct NonEmpty<C>(C)
where
    for<'a> &'a C: IntoIterator;

let xs = NonEmpty::try_new(vec![1, 2, 3]).unwrap();

Inline bounds and where clauses can also be combined:

#[nutype(derive(Debug, Clone))]
struct Combined<T: Clone>(T)
where
    T: Default;

Constructor visibility

Until now, ::new() / ::try_new() were always pub. In v0.7.0 you can pin the constructor to any visibility you like via constructor(visibility = ...) (#211):

use nutype::nutype;

#[nutype(
    constructor(visibility = pub(crate)),
    validate(not_empty),
    derive(Debug, AsRef),
)]
pub struct InternalId(String);

Supported values are pub, pub(crate), pub(super), pub(in path), and private. With private, the constructor is only callable from the module where the type is defined, useful for types that should only be obtained through a higher-level factory.

UTF-16 length validators

JavaScript and a few other ecosystems count string length in UTF-16 code units rather than Unicode characters or bytes. To make interop straightforward, v0.7.0 adds len_utf16_min and len_utf16_max (#162):

use nutype::nutype;

#[nutype(
    validate(len_utf16_min = 1, len_utf16_max = 280),
    derive(Debug, AsRef),
)]
pub struct Tweet(String);

These complement the existing len_char_min / len_char_max (Unicode chars) validators.

Deriving Valuable

When the valuable feature is enabled, you can now derive Valuable on your newtypes, useful for structured logging with tracing and similar instrumentation:

use nutype::nutype;
use valuable::Valuable;

#[nutype(derive(Valuable))]
pub struct Age(u32);

#[nutype(derive(Valuable))]
pub struct Name(String);

assert_eq!(format!("{:?}", Age::new(25).as_value()), "Age(25)");

Add it to your Cargo.toml as:

nutype = { version = "0.7", features = ["valuable"] }
valuable = { version = "0.1", features = ["derive"] }

Links

Nutype 0.6.2 - derive_unsafe

30 Jul 06:25
f133db2

Choose a tag to compare

What is nutype?

Nutype is a proc macro that adds sanitization_ and validation to newtypes, ensuring values always pass checks.

Changes in v0.6.2

  • [FEATURE] Introduce derive_unsafe(..) attribute to derive any arbitrary trait (requires derive_unsafe feature to be enabled).
  • [FIX] Updated the Rust edition: 2021 β†’ 2024.
  • [FIX] Improved error messages for len_char_max and len_char_min validators. They are now clearer and easier to understand.

derive_unsafe

You can now use the new derive_unsafe(..) attribute to derive arbitrary traits, including third-party ones, which are not known to nutype.

Unlike derive(..), this mechanism bypasses nutype’s internal safety checks, meaning it's possible to violate validation rules at runtime if you're deriving a trait that has methods that instantiate or mutate a value.

It requires derive_unsafe feature flag to be enabled.

Example (do not copy blindly):

use derive_more::{Deref, DerefMut};
use nutype::nutype;

#[nutype(
    derive(Debug, AsRef),
    derive_unsafe(Deref, DerefMut),
    validate(greater_or_equal = 0.0, less_or_equal = 2.0)
)]
struct LlmTemperature(f64);

fn main() {
    let mut temperature = LlmTemperature::try_new(1.5).unwrap();

    // This is not what nutype is designed for!
    *temperature = 2.5;

    // OH no, we've just violated the validation rule!
    assert_eq!(temperature.as_ref(), &2.5);
}

The takeaway: derive_unsafe gives you raw powerβ€”but you’re on your own to ensure your type’s invariants aren't broken.

On that note, have a nice day!.

Nutype 0.6.1 - tiny bug fix release

09 Feb 15:21
bac9648

Choose a tag to compare

Changes

  • Fix derive(Deserialize) for no_std (see #207)

Nutype v0.6.0 - const_fn and into_iterator

02 Feb 17:23
93f4bf5

Choose a tag to compare

What is nutype?

Nutype is a proc macro that adds sanitizatio_ and validation to newtypes, ensuring values always pass checks, even with serde deserialization.

Changes in v0.6.0

  • [FEATURE] You can now instantiate types in a const context if they are declared with the const_fn flag.
  • [FEATURE] You can now derive IntoIterator for types wrapping inner types that implement IntoIterator.
  • [FIX] &'a str is now supported as an inner type.
  • [BREAKING] The fallible ::new() constructor has been removed (it was deprecated in 0.4.3).

Const functions

The #[nutype] macro can now accept the const_fn flag, which instructs it to generate const fn new() / const fn try_new() functions. This allows you to create instances in a const context:

use nutype::nutype;

#[nutype(
    const_fn,
    validate(greater_or_equal = -1.0, less_or_equal = 1.0)
)]
struct Correlation(f64);

// Since Result::unwrap() is not yet supported in a const context, we need to handle the result manually:
const ZERO_CORRELATION: Correlation = match Correlation::try_new(0.0) {
    Ok(c) => c,
    Err(_) => panic!("Invalid Correlation value"),
};

Because manually unwrapping in a const context can be tedious, you can use a helper macro like the one below (not part of nutype):

macro_rules! nutype_const {
    ($name:ident, $ty:ty, $value:expr) => {
        const $name: $ty = match <$ty>::try_new($value) {
            Ok(value) => value,
            Err(_) => panic!("Invalid value"),
        };
    };
}

nutype_const!(ZERO_CORRELATION, Correlation, 0.0);

IntoIterator

Types that wrap a collection can now derive IntoIterator. This automatically provides both a consuming iterator (impl IntoIterator for T) and an iterator over references (impl IntoIterator for &T).

Example:

use nutype::nutype;

#[nutype(derive(IntoIterator))]
struct Names(Vec<String>);

fn main() {
    let names = Names::new(vec![
        "Alice".to_string(),
        "Bob".to_string(),
    ]);

    // Iterate over references
    for name in &names {
        println!("{}", name);
    }

    // Iterate over owned values (consuming iterator)
    for name in names {
        println!("{}", name);
    }
}

Nutype 0.5.1 - enhanced no_std support and bug fixes

20 Dec 11:49

Choose a tag to compare

I am excited to announce the release of Nutype 0.5.1, which brings some fixes for an improved developer experience. Below is an overview of what's included in this version:

πŸš€ New Features

  • no_std Support for ::core::error::Error
    Nutype now generates an implementation of ::core::error::Error in no_std environments when using Rust version 1.81 or higher. This enhancement ensures better compatibility with modern Rust ecosystems.

πŸ› οΈ Bug Fixes

  • Custom Error Paths
    You can now specify custom errors using a path, providing greater flexibility for your error-handling needs.
    (#186, #187)

  • Deserialize Derive Compatibility
    Resolved an issue where deriving Deserialize caused compilation errors when using both no_std and serde features.
    (#182)

  • Lint Warnings
    Fixed unnecessary lint warnings related to the inner generated module for cleaner builds.

  • Conflict with borsch Crate
    Addressed a conflict with the borsch crate to ensure seamless integration.
    (#195)

Nutype 0.5.0 with custom errors

03 Sep 07:14
aad0699

Choose a tag to compare

Changes

  • [FEATURE] Added support for custom error types and validation functions via the error and with attributes.
  • [BREAKING] Replaced lazy_static with std::sync::LazyLock for regex validation. This requires Rust 1.80 or higher and may cause compilation issues on older Rust versions due to the use of std::sync::LazyLock. If upgrading Rust isn't an option, you can still use lazy_static explicitly as a workaround.
  • [BREAKING] The fallible ::new() constructor has been fully replaced by ::try_new().

Highlights

Custom errors

Previously, custom validation logic in nutype could be achieved by passing a predicate attribute, as shown below:

#[nutype(validate(predicate = |n| n % 2 == 1))]
struct OddNumber(i64);

This would automatically generate a simple error type:

enum OddNumberError {
    PredicateViolated,
}

However, this approach often lacked flexibility. Many users needed more detailed error handling. For example, some users wanted to attach additional information to errors or provide more descriptive error messages. Others preferred to use a single error type across their application, but found it cumbersome to map very specific errors to a more general error type.

To address these needs, nutype now introduces the with attribute for custom validation functions and the error attribute for specifying custom error types:

use nutype::nutype;

// Define a newtype `Name` with custom validation logic and a custom error type `NameError`.
// If validation fails, `Name` cannot be instantiated.
#[nutype(
    validate(with = validate_name, error = NameError),
    derive(Debug, AsRef, PartialEq),
)]
struct Name(String);

// Custom error type for `Name` validation.
// You can use `thiserror` or similar crates to provide more detailed error messages.
#[derive(Debug, PartialEq)]
enum NameError {
    TooShort { min: usize, length: usize },
    TooLong { max: usize, length: usize },
}

// Validation function for `Name` that checks its length.
fn validate_name(name: &str) -> Result<(), NameError> {
    const MIN: usize = 3;
    const MAX: usize = 10;
    let length = name.encode_utf16().count();

    if length < MIN {
        Err(NameError::TooShort { min: MIN, length })
    } else if length > MAX {
        Err(NameError::TooLong { max: MAX, length })
    } else {
        Ok(())
    }
}

With this enhancement, users have full control over the error type and how errors are constructed during validation, making the error handling process more powerful and adaptable to different use cases.

Transition from fallible::new() to ::try_new()

In version 0.4.3, the fallible ::new() constructor was deprecated but still available. Now, it has been fully replaced by ::try_new(). For example, to initialize a Name from the previous example:

let name = Name::try_new("Anton").unwrap();

This change ensures a more consistent and explicit error-handling approach when creating instances.
Note that ::new() is still used as a non-fallible constructor if there a newtype has no validation.

The sponsors ❀️

A big shoutout to the true sponsors of this release - my in-laws! Thanks for taking care of my wife and kid, giving me a free weekend to work on Nutype!

Links

Nutype 0.4.3

08 Jul 07:08

Choose a tag to compare

Changes

  • Support generics
  • [DEPRECATION] Fallible (when a newtype has validation) constructor ::new() is deprecated. Users should use ::try_new() instead.
  • [FIX] Use absolute path for ::core::result::Result when generating code for derive(TryFrom).

Highlights

This release comes with support of generic types for newtypes!
The example below defines SortedNotEmptyVec<T> wrapper around Vec<T> which is guaranteed to be not empty and sorted.
Note, that type bound T: Ord enables invocation of v.sort() in the sanitization function.

use nutype::nutype;

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

let wise_friends = SortedNotEmptyVec::try_new(vec!["Seneca", "Zeno", "Plato"]).unwrap();
assert_eq!(wise_friends.as_ref(), &["Plato", "Seneca", "Zeno"]);

let numbers = SortedNotEmptyVec::try_new(vec![4, 2, 7, 1]).unwrap();
assert_eq!(numbers.as_ref(), &[1, 2, 4, 7]);

Links

Nutype 0.4.2

10 Apr 06:14

Choose a tag to compare

Changes

  • Support no_std ( the dependency needs to be declared as nutype = { default-features = false } )
  • Support integration with arbitrary crate (see arbitrary feature).
    • Support Arbitrary for integer types
    • Support Arbitrary for float types
    • Support Arbitrary for string inner types
    • Support Arbitrary for any inner types
  • Possibility to specify boundaries (greater, greater_or_equal, less, less_or_equal, len_char_min, len_char_max) with expressions or named constants.
  • Add #[inline] attribute to trivial functions
  • Improve error messages

Highlights

Here is an example of nutype and arbitrary playing together:

use nutype::nutype;
use arbtest::arbtest;
use arbitrary::Arbitrary;

#[nutype(
    derive(Arbitrary, AsRef),
    sanitize(trim),
    validate(
        not_empty,
        len_char_max = 100,
    ),
)]
pub struct Title(String);

fn main() {
    arbtest(|u| {
        // Generate an arbitrary valid Title
        let title = Title::arbitrary(u)?;

        // The inner string is guaranteed to be non-empty
        assert!(!title.as_ref().is_empty());

        // The inner string is guaranteed not to exceed 100 characters
        assert!(title.as_ref().chars().count() <= 100);
        Ok(())
    });
}

As you can see the derived implementation of Arbitrary respects the validation rules.
In the similar way Arbitrary can be derived for integer and float based types.

Links

Nutype 0.4.0

21 Nov 20:24

Choose a tag to compare

Acknowledgements

A heartfelt thanks to Daniyil Glushko for his invaluable assistance and exceptional work on this release. Daniyil, located in Zaporizhzhia, Ukraine, is a proficient Rust developer open to remote opportunities. I highly recommend reaching out to him for Rust development roles.

Changes

  • [FEATURE] Support of arbitrary inner types with custom sanitizers and validators.
  • [FEATURE] Add numeric validator greater
  • [FEATURE] Add numeric validator less
  • [BREAKING] Removal of asterisk derive
  • [BREAKING] Use commas to separate high level attributes
  • [BREAKING] Traits are derived with #[nutype(derive(Debug))]. The regular #[derive(Debug)] syntax is not supported anymore.
  • [BREAKING] Validator with has been renamed to predicate to reflect the boolean nature of its range
  • [BREAKING] String validator min_len has been renamed to len_char_min to reflect that is based on UTF8 chars.
  • [BREAKING] String validator max_len has been renamed to len_char_max to reflect that is based on UTF8 chars.
  • [BREAKING] Rename numeric validator max to less_or_equal
  • [BREAKING] Rename numeric validator min to greater_or_equal
  • [BREAKING] Rename error variants to follow the following formula: <ValidationRule>Violated. This implies the following renames:
    • TooShort -> LenCharMinViolated
    • TooLong -> LenCharMaxViolated
    • Empty -> NotEmptyViolated
    • RegexMismatch -> RegexViolated
    • Invalid -> PredicateViolated
    • TooBig -> LessOrEqualViolated
    • TooSmall -> GreaterOrEqualViolated
    • NotFinite -> FiniteViolated
  • Better error messages: in case of unknown attribute, validator or sanitizer the possible values are listed.
  • [FIX] Make derived Deserialize work with RON format

Feature highlights

Arbitrary inner type

Previously #[nutype] worked only with String, integers and floats.
Now it's possible to use it with any arbitrary type (e.g. Vec<String>):

#[nutype(
    validate(predicate = |friends| !friends.is_empty() ),
)]
pub struct Frieds(Vec<String>);

New numeric validators

Instead of former min and max integers and floats can now be validated with:

  • greater_or_equal - Inclusive lower bound
  • greater - Exclusive lower bound
  • less_or_equal - Inclusive upper bound
  • less - Exclusive upper bound

Example:

#[nutype(
    validate(
        greater_or_equal = 0,
        less_or_equal = 59,
    ),
)]
pub struct Minute(u8);

Derive

Deriving of traits now has to be done explicitly with #[nutype(derive(...))] instead of #[derive(...)]:

Example:

#[nutype(
    validate(with = |n| n % 2 == 1),
    derive(Debug, Clone, Copy)
)]
pub struct OddNumber(u64);

This makes it clear, that deriving is fully handled by #[nutype] and prevents a potential confusion.

Links

Nutype 0.3.1

30 Jun 05:04

Choose a tag to compare

Changes

  • Add ability to derive Deref on String, integer and float based types.

Examples

use nutype::nutype;

#[nutype]
#[derive(Deref)]
struct Email(String);

let email = Email::new("foo@bar.com")

// Call .len() which is delegated to the inner String due to the deref-coercion mechanism
assert_eq!(email.len(), 11);