#validation #enums #clamp #checked #integer-value #generate

checked-rs

A library for encoding validation semantics into the type system

10 releases (1 stable)

1.0.0 Jul 21, 2024
0.7.2 Jun 17, 2024
0.6.0 Jun 15, 2024
0.5.0 Jun 15, 2024
0.1.0 Jun 11, 2024

#242 in Rust patterns

MIT/Apache

53KB
1K SLoC

checked-rs

checked-rs (referred to as checked) is a Rust library that includes a generic type for encoding arbitrary validation logic into the type system along with a proc-macro for generating specialized integer types.

This library was extracted from a larger side-project to make it generally available and showcase Rust skills and knowledge.

Installation

The checked library is compatible with rustc 1.79.0-nightly (a07f3eb43 2024-04-11) but does not use any opt-in language features. To install checked-rs, add the following to your Cargo.toml:

[dependencies]
checked-rs = "1.0"

Overview

The main components of this library is the the attribute macro clamped and the View struct (plus the Validator trait). Additionally, there are some traits and types such as Behavior and ClampGuard that either configure how overflow is handled or provide an alternative way to interact with the clamped types.

clamped Functional Macro

This proc-macro facilitates the creation of specialized integer types tailored to specific constraints and use cases, enhancing type safety and code clarity in Rust projects. The generated types can be divided into two primary categories:

1. Wrapper Type

This category creates a type that wraps around any sized integer, preserving the same memory layout. The wrapper type can optionally encode an upper and/or lower bound for the integer value. It further divides into two sub-categories:

Hard Wrapper

  • Invariant: The value is guaranteed never to exceed the specified bounds.
  • Usage: Ideal for situations where strict boundary enforcement is required at the type level.

Soft Wrapper

  • Flexibility: The value can be any valid integer for the underlying type.
  • Methods: Implements methods to check whether the value lies within the specified bounds.
  • Usage: Suitable for cases where boundary checks are needed but enforced through runtime methods rather than at the type level.

2. Enum Type

This category generates an enum type over any sized integer, with variants that describe:

  • Exact values.
  • Ranges of values.
  • Nested enum types.

The top-level enum type also supports the same behavior as the "Hard" wrapper type if the variants imply an upper and/or lower bound. Additionally, this category recursively generates types to support the ranges and nested enums described by the variants.

  • Exact Values: Specific integer values represented as individual variants.
  • Ranges: Variants that encompass a range of values.
  • Nested Enums: Variants that are enums themselves, allowing for complex and hierarchical type definitions.

Usage and Configuration

The category of the type generated by the proc-macro is determined by the kind of language item specified in the input, either struct or enum.

Specifying the Target Integer Type

The target integer type is provided in an attribute above the language item. The content within the brackets of the attribute is parsed into various configuration options, allowing users to tailor the generated type to their specific needs. Here are some examples of how to use the attribute:

For the remainder of these docs, int will be used to refer to the integer type used for the clamped value.

Generating Wrappers

Specifying the Range

The type should have exactly one unnamed field, but instead of declaring a type, provide the range the type covers. All range forms are allowed.

#[usize]
struct Exclusive(10..100);

#[usize]
struct Inclusive(10..=100);

#[usize]
struct OpenEnd(10..);

#[usize]
struct OpenStart(..100);

Basic Attribute

To specify a basic wrapper type without additional configuration:

#[usize]

Specifying Soft or Hard Behavior

For wrapper types, you can specify whether the behavior should be Soft or Hard:

  • Soft Behavior:
#[usize as Soft]

This allows the value to be any valid integer for the underlying type, with methods to check if it is within bounds.

  • Hard Behavior:
#[usize as Hard]

This enforces that the value can never be outside the specified bounds.

  • Additional Derive Macros Additional derive macros can be applied to the generated type to include other traits. For example, to derive the Debug trait in addition to the always-derived Clone and Copy traits:
#[usize as Soft; derive(Debug)]

Default Value

The proc-macro allows specifying a default value for the generated type. This default value can either be inferred or manually specified.

Inferred Default Value

The default value can be inferred to be the lowest value of the range specified.

Manually Specified Default Value

Alternatively, you can manually specify the default value using an attribute. Here is an example of how to specify a default value:

#[usize; default = 10]

Automatically Generated Traits

The proc-macro automatically generates various trait definitions for the generated types, ensuring they integrate seamlessly with Rust's type system and standard library. The following traits are implemented:

  • PartialEq against itself and against the underlying type.
  • Eq
  • PartialOrd against itself and against the underlying type.
  • Ord
  • Both soft and hard versions implement Deref and AsRef<int>.
  • Only soft versions implement DerefMut and AsMut<int>.
  • Any applicable conversion traits to and from other built-in integer types.
  • Binary operations: Add, Sub, Mul, Div, Rem, BitAnd, BitOr, BitXor, and any applicable ___Assign versions of these traits.

Example Usage with Traits

Here is an example of how the generated types can be used with the automatically implemented traits:

// Define a soft wrapper type over usize with a default value of 10
#[usize as Soft; default = 10; derive(Debug)]
struct MySoftBoundedIntWithDefault(0..100);

// Define a hard wrapper type over u32 with a default value of 0
#[u32 as Hard; default = 0]
struct MyHardBoundedIntWithDefault(0..100);

# fn main() {
let a = MySoftBoundedIntWithDefault::default();
let b = MySoftBoundedIntWithDefault::from(15);
let c = MyHardBoundedIntWithDefault::default();

// PartialEq and PartialOrd
assert!(a != b);
assert!(a < b);

// Deref and AsRef
assert_eq!(*a, 10);
assert_eq!(a.as_ref(), &10);

// DerefMut and AsMut (soft only)
let mut d = b;
*d = 20;
assert_eq!(*d, 20);
assert_eq!(d.as_mut(), &mut 20);

// Binary operations
let sum = a + d;
let product = c * 5;
assert_eq!(*sum, 30);
assert_eq!(*product, 0);
# }

Generating Enums

documentation in-progress

View

The View struct is a wrapper around a value that encodes it's validation logic into the wrapper. The Validator trait is used to define the validation logic for a View. This wrapper is lightweight and can be used in place of the raw value via the Deref and/or AsRef traits.

# use checked_rs::prelude::*;

#[derive(Clone, Copy)]
struct NotSeven;

impl Validator for NotSeven {
    type Item = i32;
    type Error = anyhow::Error;

    fn validate(item: &Self::Item) -> Result<()> {
        if *item == 7 {
            Err(anyhow::anyhow!("Value must not be 7"))
        } else {
            Ok(())
        }
    }
}

let mut item = View::with_validator(0, NotSeven);
let mut g = item.modify();

*g = 7;
assert_eq!(*g, 7);
assert!(g.check().is_err());

*g = 10;
assert!(g.commit().is_ok());

// the guard is consumed by commit, so we can't check it again
// the `View`'s value should be updated
assert_eq!(&*item, &10);

Dependencies

~6.5MB
~117K SLoC