nutype

repository·master·Indexed 23 days ago

https://github.com/greyblake/nutype

A Rust procedural macro that implements the newtype pattern with built-in sanitization and validation guarantees. It ensures values are only instantiated via `::try_new()` if they meet specific constraints, supporting inner types such as String, Integers, Floats, and rust_decimal::Decimal. Nutype provides features for regex validation, custom sanitizers, custom validators, and safe trait derivation (including Eq and Ord for finite floats).

Tokens
8.9K
Snippets
25
Records
34
Agent score
78%

What's inside nutype

  1. When to use nutype

    master

    Use nutype if:

    • You use the newtype pattern to leverage the type system for business logic correctness.
    • You want to use the type system to hold invariants.
    • You follow Domain-Driven Design (DDD) and want more expressive domain models.
    • You want to prototype quickly without sacrificing type safety.

    Avoid nutype if:

    • Compiler time is a critical concern (nutype uses heavy proc-macros).
    • You prefer to avoid metaprogramming and implicit magic.
    • You rely heavily on IDE hints (IDEs may struggle with proc-macro expansion).
    • You need extreme performance and want to avoid the overhead of running validation when loading data (e.g., from a database).
  2. How Nutype inner types work

    master

    The available sanitizers, validators, and derivable traits depend on the type wrapped by the newtype. Nutype categorizes inner types into:

    • String: Currently supports only the owned String type.
    • Integer: Supports u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, usize, and isize.
    • Float: Supports f32 and f64.
    • Decimal: Supports rust_decimal::Decimal (requires the rust_decimal feature).
    • Anything else: Generic support for other types (with limited built-in features).
  3. Use `rust_decimal::Decimal` as an inner type

    master

    You can use rust_decimal::Decimal as the inner type for a nutype by enabling the rust_decimal feature flag.

    Requirements:

    1. Enable the rust_decimal feature in nutype.
    2. Add rust_decimal as a direct dependency in your Cargo.toml.
    3. For derive(Arbitrary), enable the rust-fuzz feature of rust_decimal.
    4. For derive(Serialize, Deserialize), enable the serde feature of rust_decimal.

    Note on Bounds: When using validate attributes (like less_or_equal), bounds are written as bare literals and parsed at compile time. However, when calling try_new(), you must pass actual Decimal instances (e.g., using dec!(...) or Decimal::from(...)).

    use nutype::nutype;
    use rust_decimal::Decimal;
    
    #[nutype(
        validate(greater_or_equal = 0, less_or_equal = 100),
        derive(Debug, Clone, Copy, PartialEq, PartialOrd, Display),
    )]
    pub struct Percentage(Decimal);
  4. Derive Eq and Ord for Floats

    master

    By default, floating-point types do not implement Eq or Ord because of NaN. However, if you use the finite validator, Nutype can safely derive these traits because NaN is excluded from valid values.

    #[nutype(
        validate(finite),
        derive(PartialEq, Eq, PartialOrd, Ord),
    )]
    struct Size(f64);
  5. Quick start with Nutype

    master

    Nutype is a proc macro that adds sanitization and validation to the Rust newtype pattern. It generates a struct that can only be instantiated via ::try_new(), ensuring that all constraints are met. If validation fails, it returns a generated error enum (e.g., UsernameError) containing specific error variants for each violated rule. Sanitized values are stored within the newtype.

    use nutype::nutype;
    
    // Define newtype Username
    #[nutype(
        sanitize(trim, lowercase),
        validate(not_empty, len_char_max = 20),
        derive(Debug, PartialEq, Clone),
    )]
    pub struct Username(String);
    
    // We can obtain a value of Username with `::try_new()`.
    // Note that Username holds a sanitized string
    assert_eq!(
        Username::try_new("   FooBar  ").unwrap().into_inner(),
        "foobar"
    );
    
    // It's impossible to obtain an invalid Username
    // Note that we also got `UsernameError` enum generated implicitly
    // based on the validation rules.
    assert_eq!(
        Username::try_new("   "),
        Err(UsernameError::NotEmptyViolated),
    );
    assert_eq!(
        Username::try_new("TheUserNameIsVeryVeryLong"),
        Err(UsernameError::LenCharMaxViolated),
    );
  6. Validate strings with Regex

    master

    To use regex validation, you must enable the regex feature in nutype and include the regex crate in your dependencies. You can provide the regex pattern directly as a string literal or use a pre-compiled regex from std::sync::LazyLock, lazy_static, or once_cell.

    // Inline regex
    #[nutype(validate(regex = "^[0-9]{3}-[0-9]{3}$"))]
    pub struct PhoneNumber(String);
    
    // Using std::sync::LazyLock
    use regex::Regex;
    static PHONE_NUMBER_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("^[0-9]{3}-[0-9]{3}$").unwrap());
    
    #[nutype(validate(regex = PHONE_NUMBER_REGEX))]
    pub struct PhoneNumber(String);
    
    // Using lazy_static
    use lazy_static::lazy_static;
    use regex::Regex;
    
    lazy_static! {
        static ref PHONE_NUMBER_REGEX: Regex = Regex::new("^[0-9]{3}-[0-9]{3}$").unwrap();
    }
    
    #[nutype(validate(regex = PHONE_NUMBER_REGEX))]
    pub struct PhoneNumber(String);
    
    // Using once_cell
    use once_cell::sync::Lazy;
    use regex::Regex;
    
    static PHONE_NUMBER_REGEX: Lazy<Regex> =
        Lazy::new(|| Regex::new("[0-9]{3}-[0-9]{3}$").unwrap());
    
    #[nutype(validate(regex = PHONE_NUMBER_REGEX))]
    pub struct PhoneNumber(String);
  7. Derive traits for nutypes

    master

    There are two primary ways to derive traits:

    1. derive(..): The recommended approach. It ensures derived traits do not compromise the type's invariants. Only a predefined set of traits is supported (e.g., Debug, Clone, PartialEq, Serialize, Deserialize).
    2. derive_unchecked(..): (Requires derive_unchecked feature). Allows deriving arbitrary traits, including third-party ones. Caution: Nutype cannot verify that these traits preserve invariants (e.g., they might allow mutable access to the inner value). Use with care.

    You can also use cfg_attr to conditionally derive traits based on compilation flags.

    #[nutype(derive(Debug))]
    pub struct Username(String);
    
    #[nutype(derive_unchecked(std::fmt::Debug))]
    pub struct Username;
    
    #[nutype(
        derive(Debug, PartialEq),
        cfg_attr(test, derive(Clone)),
    )]
    pub struct Email(String);
  8. Custom validation with a custom error type

    master

    To use a custom error type for validation failures, use the with and error attributes. The validation function must return Result<(), YourErrorType>.

    #[derive(Debug, PartialEq)]
    enum NameError {
        TooShort,
        TooLong,
    }
    
    fn validate_name(name: &str) -> Result<(), NameError> {
        if name.len() < 3 {
            Err(NameError::TooShort)
        } else if name.len() > 10 {
            Err(NameError::TooLong)
        } else {
            Ok(())
        }
    }
    
    #[nutype(
        validate(with = validate_name, error = NameError),
        derive(Debug, PartialEq),
    )]
    struct Name(String);
  9. Use generics and where clauses in nutypes

    master

    Nutype supports generic newtypes, including the use of where clauses and Higher-Ranked Trait Bounds (HRTB).

    Generic Newtype with Sanitization and Validation:

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

    Generic Newtype with HRTB:

    #[nutype(
        validate(predicate = |c| c.into_iter().next().is_some()),
        derive(Debug)
    )]
    struct NonEmpty<C>(C)
    where
        for<'a> &'a C: IntoIterator;
    use nutype::nutype;
    
    #[nutype(
        sanitize(with = |mut v| { v.sort(); v }),
        validate(predicate = |vec| !vec.is_empty()),
        derive(Debug, PartialEq, AsRef, Deref),
    )]
    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"]);
    
    #[nutype(
        validate(predicate = |c| c.into_iter().next().is_some()),
        derive(Debug)
    )]
    struct NonEmpty<C>(C)
    where
        for<'a> &'a C: IntoIterator;
    
    let non_empty = NonEmpty::try_new(vec![1, 2, 3]).unwrap();
    assert!(NonEmpty::try_new(Vec::<i32>::new()).is_err());
  10. Obtain a reference to the inner value

    master

    While .into_inner() consumes the newtype to return the inner type, you can use the AsRef trait to borrow the inner value without taking ownership.

    #[nutype(derive(AsRef))]
    struct Username(String);
    
    let username = Username::new("Jack");
    assert_eq!(username.as_ref(), "Jack");
  11. Derive `Eq` and `Ord` on float types

    master

    To derive Eq and Ord on floating-point types (like f32 or f64), you must include the finite validation to ensure the valid value range excludes NaN.

    #[nutype(
        validate(finite),
        derive(PartialEq, Eq, PartialOrd, Ord),
    )]
    pub struct Weight(f64);