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.
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"
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.
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:
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:
- 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.
- 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.
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.
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
.
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.
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);
To specify a basic wrapper type without additional configuration:
#[usize]
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)]
The proc-macro allows specifying a default value for the generated type. This default value can either be inferred or manually specified.
The default value can be inferred to be the lowest value of the range specified.
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]
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
andAsRef<int>
. - Only soft versions implement
DerefMut
andAsMut<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.
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);
# }
documentation in-progress
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);