• Stars
    star
    1,068
  • Rank 41,870 (Top 0.9 %)
  • Language
    Rust
  • License
    MIT License
  • Created over 1 year ago
  • Updated 5 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Rust newtype with guarantees πŸ‡ΊπŸ‡¦ πŸ¦€

Rust Nutype Logo

The newtype with guarantees.

Nutype Build Status Nutype Documentation

Philosophy

Nutype embraces the simple idea: the type system can be leveraged to track the fact that something was done, so there is no need to do it again.

If a piece of data was once sanitized and validated we can rely on the types instead of sanitizing and validating again and again when we're in doubt.

Quick start

use nutype::nutype;

#[nutype(
    sanitize(trim, lowercase)
    validate(not_empty, max_len = 20)
)]
pub struct Username(String);

Now we can create usernames:

assert_eq!(
    Username::new("   FooBar  ").unwrap().into_inner(),
    "foobar"
);

But we cannot create invalid ones:

assert_eq!(
    Username::new("   "),
    Err(UsernameError::Empty),
);

assert_eq!(
    Username::new("TheUserNameIsVeryVeryLong"),
    Err(UsernameError::TooLong),
);

Note, that we also got UsernameError enum generated implicitly.

Ok, but let's try to obtain an instance of Username that violates the validation rules:

let username = Username("".to_string())

// error[E0423]: cannot initialize a tuple struct which contains private fields
let mut username = Username::new("foo").unwrap();
username.0 = "".to_string();

// error[E0616]: field `0` of struct `Username` is private

Haha. It's does not seem to be easy!

A few more examples

Here are some other examples of what you can do with nutype.

You can skip sanitize and use a custom validator with:

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

You can skip validation, if you need sanitization only:

#[nutype(sanitize(trim, lowercase))]
struct Username(String);

In that case Username::new(String) simply returns Username, not Result.

You can derive traits. A lot of traits! For example:

#[nutype]
#[derive(*)]
struct Username(String);

The code above derives the following traits for Username: Debug, Clone, PartialEq, Eq, PartialOrd, Ord, FromStr, AsRef, Hash. * is just a syntax sugar for "derive whatever makes sense to derive by default", which is very subjective and opinionated. It's rather an experimental feature that was born from the fact that #[nutype] has to mess with #[derive] anyway because users are not supposed to be able to derive traits like DerefMut or BorrowMut. That would allow mutating the inner (protected) value which undermines the entire idea of nutype.

Inner types

Available sanitizers, validators, and derivable traits are determined by the inner type, which falls into the following categories:

  • String
  • Integer (u8, u16,u32, u64, u128, i8, i16, i32, i64, i128, usize, isize)
  • Float (f32, f64)

String

At the moment the string inner type supports only String (owned) type.

String sanitizers

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 }

String validators

Validator Description Error variant Example
max_len Max length of the string (in chars, not bytes) TooLong max_len = 255
min_len Min length of the string (in chars, not bytes) TooShort min_len = 5
not_empty Rejects an empty string Empty not_empty
regex Validates format with a regex. Requires regex feature. RegexMismatch regex = "^[0-9]{7}$" or regex = ID_REGEX
with Custom validator. A function or closure that receives &str and returns bool Invalid with = |s: &str| s.contains('@')

Regex validation

Requirements:

  • regex feature of nutype is enabled.
  • You crate have to explicitly include regex and lazy_static dependencies.

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 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);

String derivable traits

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.

Integer

The integer inner types are: u8, u16,u32, u64, u128, i8, i16, i32, i64, i128, usize, isize.

Integer sanitizers

Sanitizer Description Example
with Custom sanitizer. with = |raw| raw.clamp(0, 100)

Integer validators

Validator Description Error variant Example
max Maximum valid value TooBig max = 99
min Minimum valid value TooSmall min = 18
with Custom validator Invalid with = |num| num % 2 == 0

Integer derivable traits

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.

Float

The float inner types are: f32, f64.

Float sanitizers

Sanitizer Description Example
with Custom sanitizer. with = |val| val.clamp(0.0, 100.0)

Float validators

Validator Description Error variant Example
max Maximum valid value TooBig max = 100.0
min Minimum valid value TooSmall min = 0.0
finite Check against NaN and infinity NotFinite finite
with Custom validator Invalid with = |val| val != 50.0

Float derivable traits

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);

Custom sanitizers

You can set custom sanitizers using the with option. A custom sanitizer is a function or closure that receives a value of an inner type with ownership and returns a sanitized value.

For example, this one

#[nutype(sanitize(with = new_to_old))]
pub struct CityName(String);

fn new_to_old(s: String) -> String {
    s.replace("New", "Old")
}

is equal to the following one:

#[nutype(sanitize(with = |s| s.replace("New", "Old") ))]
pub struct CityName(String);

And works the same way:

let city = CityName::new("New York");
assert_eq!(city.into_inner(), "Old York");

Custom validators

In similar fashion it's possible to define custom validators, but a validation function receives a reference and returns bool. Think of it as a predicate.

#[nutype(validate(with = is_valid_name))]
pub struct Name(String);

fn is_valid_name(name: &str) -> bool {
    // A fancy way to verify if the first character is uppercase
    name.chars().next().map(char::is_uppercase).unwrap_or(false)
}

Deriving recipes

Deriving Default

#[nutype(default = "Anonymous")]
#[derive(Default)]
pub struct Name(String);

Deriving Eq and Ord on float types

With nutype it's possible to derive Eq and Ord if there is finite validation set. The finite validation ensures that the valid value excludes NaN.

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

How to break the constraints?

First you need to know, you SHOULD NOT do it.

But let's pretend for some imaginary performance reasons you really need to avoid validation when instantiating a value of newtype (e.g. loading earlier "validated" data from DB).

You can achieve this by enabling new_unchecked crate feature and marking a type with new_unchecked:

#[nutype(
    new_unchecked
    sanitize(trim)
    validate(min_len = 8)
)]
pub struct Name(String);

// Yes, you're forced to use `unsafe` here, so everyone will point fingers at YOU.
let name = unsafe { Name::new_unchecked(" boo ".to_string()) };

// `name` violates the sanitization and validation rules!!!
assert_eq!(name.into_inner(), " boo ");

Feature flags

  • serde - integrations with serde crate. Allows to derive Serialize and Deserialize traits.
  • regex - allows to use regex = validation on string-based types. Note: your crate also has to explicitly have regex and lazy_static within dependencies.
  • schemars08 - allows to derive JsonSchema trait of schemars crate. Note that at the moment validation rules are not respected.
  • new_unchecked - enables generation of unsafe ::new_unchecked() function.

When nutype is a good fit for you?

  • If you enjoy newtype pattern and you like the idea of leveraging the Rust type system to enforce the correctness of the business logic.
  • If you're a DDD fan, nutype is a great helper to make your domain models even more expressive.
  • You want to prototype quickly without sacrificing quality.

When nutype is not that good?

  • You care too much about compiler time (nutype relies on heavy usage of proc macros).
  • You think metaprogramming is too much implicit magic.
  • IDEs may not be very helpful at giving you hints about proc macros.
  • Design of nutype may enforce you to run unnecessary validation (e.g. on loading data from DB), which may have a negative impact if you aim for extreme performance.

How it works?

The following snippet

#[nutype(
    sanitize(trim, lowercase)
    validate(not_empty, max_len = 20)
)]
pub struct Username(String);

eventually is transformed into something similar to this:

// Everything is wrapped into the module,
// so the internal tuple value of Username is private and cannot be directly manipulated.
mod __nutype_private_Username__ {
    pub struct Username(String);

    pub enum UsernameError {
        // Occurs when a string is empty
        Empty,

        // Occurs when a string is longer than 255 chars.
        TooLong,
    }

    impl Username {
        // The only legit way to construct Username.
        // All other constructors (From, FromStr, Deserialize, etc.)
        // are built on top of this one.
        pub fn new(raw_username: impl Into<String>) -> Result<Username, UsernameError> {
            // Sanitize
            let sanitized_username = raw_username.into().trim().lowercase();

            // Validate
            if sanitized_username.empty() {
                Err(UsernameError::Empty)
            } else if (sanitized_username.len() > 40 {
                Err(UsernameError::TooLong)
            } else {
                Ok(Username(sanitized_username))
            }
        }

        // Convert back to the inner type.
        pub fn into_inner(self) -> String {
            self.0
        }
    }
}

pub use __nutype_private_Username__::{Username, UsernameError};

As you can see, #[nutype] macro gets sanitization and validation rules and turns them into Rust code.

The Username::new() constructor performs sanitization and validation and in case of success returns an instance of Username.

The Username::into_inner(self) allows converting Username back into the inner type (String).

And of course, the variants of UsernameError are derived from the validation rules.

But the whole point of the nutype crate is that there is no legit way to obtain an instance of Username that violates the sanitization or validation rules. The author put a lot of effort into this. If you find a way to obtain the instance of a newtype bypassing the validation rules, please open an issue.

A note about #[derive(...)]

You've got to know that the #[nutype] macro intercepts #[derive(...)] macro. It's done on purpose to ensure that anything like DerefMut or BorrowMut, that can lead to a violation of the validation rules is excluded. The library takes a conservative approach and it has its downside: deriving traits that are not known to the library is not possible.

Support Ukrainian military forces πŸ‡ΊπŸ‡¦

Today I live in Berlin, I have the luxury to live a physically safe life. But I am Ukrainian. The first 25 years of my life I spent in Kharkiv, the second-largest city in Ukraine, 60km away from the border with russia. Today about a third of my home city is destroyed by russians. My parents, my relatives and my friends had to survive the artillery and air attack, living for over a month in basements.

Some of them have managed to evacuate to EU. Some others are trying to live "normal lifes" in Kharkiv, doing there daily duties. And some are at the front line right now, risking their lives every second to protect the rest.

I encourage you to donate to Charity foundation of Serhiy Prytula. Just pick the project you like and donate. This is one of the best-known foundations, you can watch a little documentary about it. Your contribution to the Ukrainian military force is a contribution to my calmness, so I can spend more time developing the project.

Thank you.

Similar projects

  • prae - A very similar crate that aims to solve the same problems but with slightly different approach.
  • bounded-integer - Bounded integers for Rust.
  • refinement - Convenient creation of type-safe refinement types (based on generics).
  • semval - Semantic validation for Rust.
  • validator - Simple validation for Rust structs (powered by macros).

License

MIT Β© Sergey Potapov

More Repositories

1

whatlang-rs

Natural language detection library for Rust. Try demo online: https://whatlang.org/
Rust
926
star
2

ta-rs

Technical analysis library for Rust language
Rust
584
star
3

vim-preview

Vim plugin for previewing markup files(markdown,rdoc,textile,html)
Vim Script
209
star
4

envconfig-rs

Build a config structure from environment variables in Rust without boilerplate
Rust
186
star
5

blogo

Mountable blog engine for Ruby on Rails
Ruby
101
star
6

kinded

Generate Rust enum variants without associated data
Rust
81
star
7

cargo-testify

Watches changes in a rust project, runs test and shows friendly notification
Rust
80
star
8

awesome-programming-books

List of good programming books for beginners and professionals
80
star
9

mago

Magic numbers detector for Ruby source code
Ruby
58
star
10

humble-investing

List of resources that I use for investing research
41
star
11

jsonpath-rs

JSONPath for Rust
Rust
37
star
12

from-typescript-to-rescript

Frontend of https://Inhyped.com written in TypeScript and rewritten in ReScript
TypeScript
29
star
13

telebot

Ruby client for Telegram bot API
Ruby
28
star
14

fast_seeder

Speed up seeding your Rails application using multiple SQL inserts!
Ruby
25
star
15

xplan

Visualizes dependencies between tasks
Rust
22
star
16

dm-rspec

RSpec matchers for DataMapper
Ruby
19
star
17

hellcheck

HTTP health checker implemented in Rust
Rust
19
star
18

vim-esperanto

Vim plugin for typing Esperanto language in any way (Esperanto keyboard, h, x, ^)
Vim Script
15
star
19

crystal-google_translate

Google Translate client for Crystal
Crystal
14
star
20

hail

HTTP load testing tool powered by Rust
Rust
14
star
21

crystal-magma

Crystal interpreter
Crystal
13
star
22

poloniex-rs

Rust client for Poloniex API
Rust
9
star
23

whatlang-ffi

C bindings for whatlang Rust library
C
9
star
24

rustcast

Code for RustCast screencast episodes (https://www.youtube.com/channel/UCZSy_LFJOtOPPcsE64KxDkw)
Rust
8
star
25

crystal-aitk

Artificial Intelligence Tool Kit for Crystal lang
Crystal
8
star
26

tokipona

Ruby library to process constructed language Toki Pona
Ruby
8
star
27

rails3_pg_deferred_constraints

Rails 3 engine which provides a hack to avoid RI_ConstraintTrigger Error bug.
Ruby
7
star
28

dotvim

My .vim
Vim Script
4
star
29

conway-rs

Conway's Game of Life implemented in Rust.
Rust
4
star
30

arbitrary_ext

Provides a way to derive Arbitrary trait but set custom implementation for single fields if necessary.
Rust
4
star
31

crystal-glosbe

Crystal Client for Glosbe API
Crystal
3
star
32

rails_markdown

Allows you to use markdown templates with placeholders in rails application}
Ruby
3
star
33

vim-smartdict

Vim plugin to use translate words (dictionary).
Vim Script
3
star
34

dot-nvim

My nvim config
Vim Script
2
star
35

enum_param-rs

Rust
2
star
36

beep-alarm

Alarm written in bash and based on beep tool
2
star
37

crystal-delemma

Lemmatization tool for German language.implemented in Crystal
Crystal
2
star
38

crystal-telegram_bot

Crystal
2
star
39

whatlang-website

Website for whatlang (whatlang.org)
JavaScript
2
star
40

crystal-icr

Interactive console for Crystal programming language
2
star
41

crystal-cossack

Simple and flexible HTTP client for Crystal with middleware and test support.
2
star
42

greyblake.github.com

My blog
HTML
1
star
43

talks

Public talks / presentations
Vue
1
star
44

alis

Tool to create more flexible aliases.
Ruby
1
star
45

rustcast-travis-demo

Rust
1
star
46

gync

Synchronize data of desktop applications with Git
Ruby
1
star
47

dm-enum

Enumerated models for DataMapper
Ruby
1
star
48

envconfig-rs-old

An easy way to build a config structure form environment variables in Rust without boilerplate.
Rust
1
star
49

crystal-jwt

JWT implementation in Crystal
1
star
50

deutscher_bot

Telegram Bot that helps to learn German, implemented in Crystal
Crystal
1
star