Crate tightness[][src]

Expand description

This crate provides a way to define type wrappers that behave as close as possible to the underlying type, but guarantee to uphold arbitrary invariants at all times.

Example

use tightness::{bound, Bounded};
bound!(Username: String where |s| s.len() < 8);

The bound macro invocation above defines a Username type (actually, a type alias of Bounded<String, UsernameBound>) that is a thin wrapper around String, with the added promise that it will always have less than eight characters.

Immutable access behaves as close as possible to the underlying type, with all traits you’d expect from a newtype wrapper already implemented:

assert!(username.chars().all(char::is_alphanumeric));
let solid_credentials = format!("{}:{}", *username, "Password");

However, construction and mutable access must be done through a fixed set of forms that ensure the invariants are always upheld:

use tightness::{self, bound, Bounded};
bound!(Username: String where |s| s.len() < 8);

// The only constructor is fallible, and the input value must satisfy
// the bound conditions for it to succeed.
assert!(matches!(Username::new("Far_too_long".to_string()),
   Err(tightness::ConstructionError(_))));
let mut username = Username::new("Short".to_string()).unwrap();

// In-place mutation panics if the invariant is broken:
// Would panic: `username.mutate(|u| u.push_str("Far_too_long"))`
username.mutate(|u| *u = u.to_uppercase());
assert_eq!(*username, "SHORT");

// If the underlying type implements `clone`, you can try non-destructive,
// fallible mutation at the cost of one copy:
assert!(matches!(username.try_mutate(|u| u.push_str("Far_too_long")),
   Err(tightness::MutationError(None))));
assert_eq!(*username, "SHORT");

// You can also attempt mutation by providing a fallback value
let fallback = username.clone();
assert!(matches!(username.mutate_or(fallback, |u| u.push_str("Far_too_long")),
   Err(tightness::MutationError(None))));
assert_eq!(*username, "SHORT");

// Finally, you can just pass by value, and the inner will be recoverable if mutation fails
assert!(matches!(username.into_mutated(|u| u.push_str("Far_too_long")),
    Err(tightness::MutationError(Some(_)))));

Performance

Since invariants are arbitrarily complex, it’s not possible to guarantee they’re evaluated at compile time. Using a Bounded type incurs the cost of invoking the invariant function on construction and after every mutation. However, the function is known at compile time, so it’s possible for the compiler to elide it in the trivial cases.

Complex mutations consisting of multiple operations can be batched in a single closure, so that the invariant is enforced only once at the end. Be careful however: while the closure is executing, the value is considered to be mid-mutation and the invariant may not hold. Don’t use the inner value to trigger any side effects that depend on it being correct.

Enabling the feature flag unsafe_access expands Bounded types with a set of methods that allow unsafe construction and mutation, requiring you to uphold the invariants manually. It also offers a verify method that allows you to check the invariants at any time. This can help in the cases where maximum performance is needed, but it must be used with caution.

Without Macros

The bound macro simplifies defining bound types, but it’s also possible to define them directly. The following is equivalent to bound!(pub NonZero: usize where |u| u > 0);

#[derive(Debug)]
pub struct NonZeroBound;

impl tightness::Bound for NonZeroBound {
    type Target = usize;
    fn check(target: &usize) -> bool { *target > 0 }
}

pub type NonZero = tightness::Bounded<usize, NonZeroBound>;

The bound is associated to the type, and will then be called on construction and after mutation of any value of type NonZero.

Macros

bound

Convenience macro that defines a bounded type, which is guaranteed to always uphold an invariant expressed as a boolean function. The resulting type is an alias of Bounded<BaseType, TypeNameBound>.

Structs

Bounded

A bounded type, i.e. a thin wrapper around an inner type that guarantees a specific invariant is always held. Generic over an inner type T and a Bound that targets it.

ConstructionError

The result of a failed invariant check on construction. Contains the value that failed to uphold the invariant.

MutationError

The result of a failed invariant check at the end of mutation.

Traits

Bound

Trait for an arbitrary condition that a bounded type must guarantee to uphold at all times.