Crate prae

source · []
Expand description

What is prae?

This crate aims to provide a better way to define types that require validation. prae is not a validation library, but a library that helps developers to define validation-requiring types with very little effort.

How it works?

The main way to use prae is through define! macro.

For example, suppose you want to create a Username type. You want this type to be a string, and you don’t want it to be empty. Traditionally, would create a wrapper struct with getter and setter functions, like this simplified example:

#[derive(Debug)]
pub struct Username(String);

impl Username {
    pub fn new(username: &str) -> Result<Self, &'static str> {
        let username = username.trim().to_owned();
        if username.is_empty() {
            Err("value is invalid")
        } else {
            Ok(Self(username))
        }
    }

    pub fn get(&self) -> &str {
        &self.0
    }

    pub fn set(&mut self, username: &str) -> Result<(), &'static str> {
        let username = username.trim().to_owned();
        if username.is_empty() {
            Err("value is invalid")
        } else {
            self.0 = username;
            Ok(())
        }
   }
}

let username = Username::new(" my username ").unwrap();
assert_eq!(username.get(), "my username");

let err = Username::new("  ").unwrap_err();
assert_eq!(err, "value is invalid");

Using prae, you will do it like this:

use prae::define;

define! {
    pub Username: String
    adjust |username| *username = username.trim().to_owned()
    ensure |username| !username.is_empty()
}

let username = Username::new(" my username ").unwrap();
assert_eq!(username.get(), "my username");

let err = Username::new("  ").unwrap_err();
assert_eq!(err.inner, "value is invalid");
assert_eq!(err.value, "");

Futhermore, prae allows you to use custom errors and extend your types. See docs for define! for more information and examples.

Additional features

*_unprocessed functions

By default, all methods of the wrappers generated by define! (which are just aliases of the Bounded type) will run the adjustment/validation (if present) routines on every construction and mutation.

If you find yourself in a situation where you know for sure that some construction/mutation is valid, you can opt out of this using *_unprocessed functions (e.g. foo.set_unprocessed(value) instead of foo.set(value)) and save a bit of computations.

To be able to use these functions, just enable the unprocessed feature of the crate.

Serde integration

You can enable serde integration with the serde feature. It will implement Serialize and Deserialize traits for the wrappers if their inner type implements them. The deserialization will automatically fail if it contains invalid value. Here is an example:

use serde::{Deserialize, Serialize};
use prae::define;

define! {
    Username: String
    adjust   |username| *username = username.trim().to_string()
    validate(&'static str) |username| {
        if username.is_empty() {
            Err("username is empty")
        } else {
            Ok(())
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
struct User {
    username: Username,
}

// Serialization works as expected.
let u = User {
    username: Username::new("  john doe  ").unwrap(),
};
let j = serde_json::to_string(&u).unwrap();
assert_eq!(j, r#"{"username":"john doe"}"#);

// Deserialization with invalid data fails.
let e = serde_json::from_str::<User>(r#"{ "username": "  " }"#).unwrap_err();
assert_eq!(e.to_string(), "username is empty at line 1 column 20");

// And here we get a nice adjusted value.
let u = serde_json::from_str::<User>(r#"{ "username": "  john doe  " }"#).unwrap();
assert_eq!(u.username.get(), "john doe");

Drawbacks

Although proc macros are very powerful, they aren’t free. In this case, you have to pull up additional dependencies such as syn and quote, and expect a slightly slower compile times.

Macros

A macro that makes defining new types easy.

Structs

A thin wrapper around an inner type that utilizes Bound B to ensure the inner value is always valid.

An error that occurs when bounded type is constructed with a value that isn’t valid. It contains some inner error that describes the reason for the error as well as the value that caused the error.

An error that occurs when bounded type is mutated in a way so that it’s inner value isn’t valid anymore. It contains some inner error that describes the reason for the error as well as both the value before mutation and the value after mutation (that caused the error).

Traits

A trait that describes a type, a process through which the type’s value must go on every construction and mutation, and a type of an error that can occur during the process.

Convenience trait that allows mapping from Result<_, ConstructionError<Bound>> and Result<_, MutationError<Bound> to Result<_, Bound::Error>.