Rust newtype with guarantees
Nutype is a proc macro that allows adding extra constraints like sanitization and validation to the regular newtype pattern. The generated code makes it impossible to instantiate a value without passing the checks. It works this way even with serde deserialization.
…For more please see:
Available sanitizers, validators, and derivable traits are determined by the inner type, which falls into the following categories:
u8, u16,u32, u64, u128, i8, i16, i32, i64, i128, usize, isize)f32, f64)rust_decimal::Decimal, requires the rust_decimal feature)At the moment the string inner type supports only String (owned) type.
| Sanitizer | Description | Example |
|---|---|---|
trim |
Removes leading and trailing whitespaces | trim |
lowercase |
Converts the string to lowercase | lowercase |
uppercase |
Converts the string to uppercase | uppercase |
with |
Custom sanitizer. A function or closure that receives String and returns String |
with = |mut s: String| ( s.truncate(5); s ) |
| Validator | Description | Error variant | Example |
|---|---|---|---|
len_char_min |
Min length of the string (in chars, not bytes) | LenCharMinViolated |
len_char_min = 5 |
len_char_max |
Max length of the string (in chars, not bytes) | LenCharMaxViolated |
len_char_max = 255 |
len_utf16_min |
Min length of the string in UTF-16 code units (useful for JavaScript interop) | LenUtf16MinViolated |
len_utf16_min = 5 |
len_utf16_max |
Max length of the string in UTF-16 code units (useful for JavaScript interop) | LenUtf16MaxViolated |
len_utf16_max = 255 |
not_empty |
Rejects an empty string | NotEmptyViolated |
not_empty |
regex |
Validates format with a regex. Requires regex feature. |
RegexViolated |
regex = "^[0-9]{7}$" or regex = ID_REGEX |
predicate |
Custom validator. A function or closure that receives &str and returns bool |
PredicateViolated |
predicate = |s: &str| s.contains('@') |
with |
Custom validator with a custom error | N/A | (see example below) |
Requirements:
regex feature of nutype is enabled.regex as a dependency.There are a number of ways you can use regex.
A regular expression can be defined right in place:
#[nutype(validate(regex = "^[0-9]{3}-[0-9]{3}$"))]
pub struct PhoneNumber(String);or it can be defined with 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);or it can be defined with 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);or 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);The following traits can be derived for a string-based type:
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, FromStr, AsRef, Deref,
From, TryFrom, Into, Hash, Borrow, Display, Default, Serialize, Deserialize.
The integer inner types are: u8, u16,u32, u64, u128, i8, i16, i32, i64, i128, usize, isize.
| Sanitizer | Description | Example |
|---|---|---|
with |
Custom sanitizer. | with = |raw| raw.clamp(0, 100) |
| Validator | Description | Error variant | Example |
|---|---|---|---|
less |
Exclusive upper bound | LessViolated |
less = 100 |
less_or_equal |
Inclusive upper bound | LessOrEqualViolated |
less_or_equal = 99 |
greater |
Exclusive lower bound | GreaterViolated |
greater = 17 |
greater_or_equal |
Inclusive lower bound | GreaterOrEqualViolated |
greater_or_equal = 18 |
predicate |
Custom predicate | PredicateViolated |
predicate = |num| num % 2 == 0 |
with |
Custom validator with a custom error | N/A | (see example below) |
The following traits can be derived for an integer-based type:
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromStr, AsRef, Deref,
Into, From, TryFrom, Hash, Borrow, Display, Default, Serialize, Deserialize.
The float inner types are: f32, f64.
| Sanitizer | Description | Example |
|---|---|---|
with |
Custom sanitizer. | with = |val| val.clamp(0.0, 100.0) |
| Validator | Description | Error variant | Example |
|---|---|---|---|
less |
Exclusive upper bound | LessViolated |
less = 100.0 |
less_or_equal |
Inclusive upper bound | LessOrEqualViolated |
less_or_equal = 100.0 |
greater |
Exclusive lower bound | GreaterViolated |
greater = 0.0 |
greater_or_equal |
Inclusive lower bound | GreaterOrEqualViolated |
greater_or_equal = 0.0 |
finite |
Check against NaN and infinity | FiniteViolated |
finite |
predicate |
Custom predicate | PredicateViolated |
predicate = |val| val != 50.0 |
with |
Custom validator with a custom error | N/A | (see example below) |
The following traits can be derived for a float-based type:
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromStr, AsRef, Deref,
Into, From, TryFrom, Hash, Borrow, Display, Default, Serialize, Deserialize.
It's also possible to derive Eq and Ord if the validation rules guarantee that NaN is excluded.
This can be done applying by finite validation. For example:
#[nutype(
validate(finite),
derive(PartialEq, Eq, PartialOrd, Ord),
)]
struct Size(f64);The rust_decimal::Decimal type is supported as an inner type behind the
rust_decimal feature flag.
Requirements:
rust_decimal feature of nutype is enabled.rust_decimal as a dependency of your crate.derive(Arbitrary) you also need rust_decimal's rust-fuzz feature, and
for derive(Serialize, Deserialize) its serde feature.All three spellings of the inner type are detected:
struct A(Decimal); // assumes `use rust_decimal::Decimal;`
struct B(rust_decimal::Decimal);
struct C(::rust_decimal::Decimal);Example:
use nutype::nutype;
use rust_decimal::Decimal;
#[nutype(
validate(greater_or_equal = 0, less_or_equal = 100), // bare literals, no dec!() needed
derive(Debug, Clone, Copy, PartialEq, PartialOrd, Display),
)]
pub struct Percentage(Decimal);Note the asymmetry: bounds in the attribute are written as bare literals (parsed
at compile time via rust_decimal's FromStr), while values passed to
try_new() are real Decimals, so you typically reach for dec!(...) or
Decimal::from(...) there.
| Sanitizer | Description | Example |
|---|---|---|
with |
Custom sanitizer. | with = |d: Decimal| d.round_dp(2) |
| Validator | Description | Error variant | Example |
|---|---|---|---|
less |
Exclusive upper bound | LessViolated |
less = 100 |
less_or_equal |
Inclusive upper bound |
No open issues yet, or sync has not completed.