tightness/lib.rs
1//! This crate provides a way to define type wrappers that behave
2//! as close as possible to the underlying type, but guarantee to
3//! uphold arbitrary invariants at all times.
4//!
5//! # Example
6//! ```
7//! use tightness::{bound, Bounded};
8//! bound!(Username: String where |s| s.len() < 8);
9//! ```
10//!
11//! The [`bound`](bound) macro invocation above defines a `Username` type (actually,
12//! a type alias of [`Bounded<String, UsernameBound>`](Bounded)) that is
13//! a thin wrapper around String, with the added promise that it will
14//! always have less than eight characters.
15//!
16//! Immutable access behaves as close as possible to the underlying type,
17//! with all traits you'd expect from a newtype wrapper already implemented:
18//!
19//! ```
20//! # use tightness::{bound, Bounded};
21//! # bound!(Username: String where |s| s.len() < 8);
22//! # let username = Username::new("Admin".to_string()).unwrap();
23//! assert!(username.chars().all(char::is_alphanumeric));
24//! let solid_credentials = format!("{}:{}", *username, "Password");
25//! ```
26//!
27//! However, construction and mutable access must be done through a fixed set of forms that
28//! ensure the invariants are *always* upheld:
29//!
30//! ```
31//! use tightness::{self, bound, Bounded};
32//! bound!(Username: String where |s| s.len() < 8);
33//!
34//! // The only constructor is fallible, and the input value must satisfy
35//! // the bound conditions for it to succeed.
36//! assert!(matches!(Username::new("Far_too_long".to_string()),
37//! Err(tightness::ConstructionError(_))));
38//! let mut username = Username::new("Short".to_string()).unwrap();
39//!
40//! // In-place mutation panics if the invariant is broken:
41//! // Would panic: `username.mutate(|u| u.push_str("Far_too_long"))`
42//! username.mutate(|u| *u = u.to_uppercase());
43//! assert_eq!(*username, "SHORT");
44//!
45//! // If the underlying type implements `clone`, you can try non-destructive,
46//! // fallible mutation at the cost of one copy:
47//! assert!(matches!(username.try_mutate(|u| u.push_str("Far_too_long")),
48//! Err(tightness::MutationError(None))));
49//! assert_eq!(*username, "SHORT");
50//!
51//! // You can also attempt mutation by providing a fallback value
52//! let fallback = username.clone();
53//! assert!(matches!(username.mutate_or(fallback, |u| u.push_str("Far_too_long")),
54//! Err(tightness::MutationError(None))));
55//! assert_eq!(*username, "SHORT");
56//!
57//! // Finally, you can just pass by value, and the inner will be recoverable if mutation fails
58//! assert!(matches!(username.into_mutated(|u| u.push_str("Far_too_long")),
59//! Err(tightness::MutationError(Some(_)))));
60//! ```
61//!
62//! # Performance
63//!
64//! Since invariants are arbitrarily complex, it's not possible to guarantee they're evaluated at
65//! compile time. Using a [`Bounded`](Bounded) type incurs the cost of invoking the invariant
66//! function on construction and after every mutation. However, the function is known at compile
67//! time, so it's possible for the compiler to elide it in the trivial cases.
68//!
69//! Complex mutations consisting of multiple operations can be batched in a single closure, so that
70//! the invariant is enforced only once at the end. Be careful however: while the closure is
71//! executing, the value is considered to be mid-mutation and the invariant may not hold. Don't use
72//! the inner value to trigger any side effects that depend on it being correct.
73//!
74//! Enabling the feature flag `unsafe_access` expands [`Bounded`](Bounded) types with a set of
75//! methods that allow unsafe construction and mutation, requiring you to uphold the invariants
76//! manually. It also offers a `verify` method that allows you to check the invariants at any time.
77//! This can help in the cases where maximum performance is needed, but it must be used with
78//! caution.
79//!
80//! # Without Macros
81//!
82//! The [`bound`](bound) macro simplifies defining bound types, but it's also possible to define
83//! them directly. The following is equivalent to `bound!(pub NonZero: usize where |u| u > 0)`;
84//!
85//! ```
86//! #[derive(Debug)]
87//! pub struct NonZeroBound;
88//!
89//! impl tightness::Bound for NonZeroBound {
90//! type Target = usize;
91//! fn check(target: &usize) -> bool { *target > 0 }
92//! }
93//!
94//! pub type NonZero = tightness::Bounded<usize, NonZeroBound>;
95//! ```
96//!
97//! The bound is associated to the type, and will then be called on construction and after mutation
98//! of any value of type `NonZero`.
99
100#![cfg_attr(not(feature = "unsafe_access"), forbid(unsafe_code))]
101pub use crate::core::*;
102mod core;
103
104#[doc(hidden)]
105pub use paste::paste;
106
107/// Convenience macro that defines a bounded type, which is guaranteed to always uphold an
108/// invariant expressed as a boolean function. The resulting type is an alias of [`Bounded<BaseType,
109/// TypeNameBound>`](Bounded).
110///
111/// # Examples
112/// ```
113/// use tightness::{bound, Bounded};
114///
115/// // Defines a public `Letter` type that wraps `char`, ensuring it's always alphabetic.
116/// bound!(pub Letter: char where |c| c.is_alphabetic());
117///
118/// // Defines a private `XorPair` type that wraps a pair of bools, so that they're never both true
119/// // or false.
120/// bound!(XorPair: (bool, bool) where |(a, b)| a ^ b);
121/// ```
122#[macro_export]
123macro_rules! bound {
124 ($visib:vis $name:ident: $type:ty where $check:expr) => {
125 $crate::paste! {
126 #[derive(Debug)]
127 $visib struct [<$name Bound>];
128
129 impl $crate::Bound for [<$name Bound>] {
130 type Target = $type;
131 fn check(target: &Self::Target) -> bool {
132 let check: fn(&Self::Target) -> bool = $check;
133 check(target)
134 }
135 }
136
137 $visib type $name = $crate::Bounded<$type, [<$name Bound>]>;
138 }
139 };
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 bound!(Password: String where |p| p.len() < 8 && p.chars().all(char::is_alphanumeric));
146 bound!(Month: usize where |m| *m < 12usize);
147 bound!(XorPair: (bool, bool) where |(a, b)| a ^ b);
148
149 impl std::ops::Add<usize> for Month {
150 type Output = Self;
151
152 fn add(mut self, rhs: usize) -> Self::Output {
153 self.mutate(|m| *m = (*m + rhs) & 12usize);
154 self
155 }
156 }
157
158 #[test]
159 #[should_panic]
160 fn invalid_bound_string_operation_panics() {
161 let mut password = Password::new("Hello".to_owned()).unwrap();
162 password.mutate(|p| p.push_str("World"));
163 }
164
165 #[test]
166 fn fallible_constructions_fail_on_invalid_input() {
167 assert!(Month::new(22).is_err());
168 assert!(Password::new("---".to_owned()).is_err());
169 assert!(XorPair::new((true, true)).is_err());
170 }
171
172 #[test]
173 fn fallible_mutations_fail_on_invalid_final_values() {
174 let mut month = Month::new(7).unwrap();
175 let impossible_mutation = |m: &mut usize| *m = *m + 13;
176 assert!(matches!(month.try_mutate(impossible_mutation), Err(MutationError(None))));
177 assert!(matches!(
178 month.mutate_or(month.clone(), impossible_mutation),
179 Err(MutationError(None))
180 ));
181 assert!(matches!(month.into_mutated(impossible_mutation), Err(MutationError(Some(20)))));
182
183 let mut xor_pair = XorPair::new((true, false)).unwrap();
184 assert!(matches!(xor_pair.try_mutate(|(a, b)| *a = *b), Err(MutationError(None))));
185 }
186
187 #[test]
188 fn fallible_mutations_succeed_on_valid_final_values() {
189 let mut month = Month::new(7).unwrap();
190 month.try_mutate(|m| *m += 1).unwrap();
191 assert_eq!(*month, 8);
192 month.mutate_or(month.clone(), |m| *m += 1).unwrap();
193 assert_eq!(*month, 9);
194 let month = month.into_mutated(|m| *m += 1).unwrap();
195 assert_eq!(*month, 10);
196 }
197
198 #[test]
199 fn convenient_operators_on_bounded_types() {
200 fn takes_as_ref<T: AsRef<usize>>(_: &T) {}
201 let month = Month::new(1).unwrap();
202 takes_as_ref(&month);
203 assert_eq!(month, month.clone());
204
205 bound!(FixedVec: Vec<bool> where |v| v.len() == 4);
206 let fixed = FixedVec::new(vec![false, true, false, false]).unwrap();
207 assert_eq!(fixed[1], true);
208 }
209}