#invariants #compile-time #properties #validation #valid #enforcing #invalid

validity

A type safe wrapper for enforcing arbitrary properties at compile time

6 releases

0.3.1 Jan 19, 2023
0.3.0 Jan 18, 2023
0.2.0 Jan 18, 2023
0.1.2 May 6, 2022
0.1.1 Feb 3, 2022

#161 in Email

Download history 23/week @ 2024-07-29 17/week @ 2024-08-05 7/week @ 2024-09-09 9/week @ 2024-09-16 25/week @ 2024-09-23 70/week @ 2024-09-30 22/week @ 2024-10-14 13/week @ 2024-10-21 47/week @ 2024-11-04

82 downloads per month

MIT license

12KB
59 lines

validity

Compile-time enforcement of arbitrary invariants

In general, it is a good idea to make invalid states unrepresentable. This can often be achieved using the type system, but there is a limit to the type system's power.

For example, imagine you're trying to represent a user's email address. You might write a struct like this:

struct Email(pub String);

But you don't want any old string to be allowed, you might want to validate that it is actually a valid email address. So your code might look like:

fn validate_email(email: &Email) -> bool {
  // ...
}

fn handle_email(email: Email) {
  if !validate_email(&email) {
    panic!("invalid email");  // or return Err(...)
  }
  
  do_stuff_with_valid_email(email: Email);
}

fn do_stuff_with_valid_email(email: Email) {
  println!("definitely a valid email: {}", email.0);
}

This works, but we're "hiding" a particular invariant from the compiler: do_stuff_with_email cannot be called with an Email that might return false when passed to validate_email.

However, with validity, we can make a few small changes to tell the compiler about this invariant:

  • Firstly, define what it means for an email to be "valid"
impl Validate for Email {
  type Error = &'static str;  // preferably a more meaningful error type
  
  fn is_valid(&self) -> Result<(), Self::Error> {
    match validate_email(self) {
      true => Ok(()),
      false => Err("invalid email"),
    }
  }
}
  • Then, we mark do_stuff_with_valid_email as needing a valid email address:
fn do_stuff_with_valid_email(email: Valid<Email>) {
  // ...
}

The only way to get a Valid<Email> is by going through the validate function and handling any potential errors:

fn handle_email(email: Email) {
  match email.validate() {
    Ok(valid_email) => do_stuff_with_valid_email(valid_email),
    Err(_) => println!("uh oh!"),
  }
}

Great! We now can't forget to validate emails, since there is no other way to create a Valid<T>.

Warning - determinism

Note, this implementation assumes that is_valid is deterministic (i.e. it gives the same result every time it's called). For example, the following implementation can allow an invalid value to exist inside a Valid<T>:

impl Valid for MyType {
  type Error = ();
  fn is_valid(&self) -> Result<(), ()> {
    match rand::random() {
      true => Ok(()),
      false => Err(()),
    }
  }
}

No runtime deps

Features