├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── core.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tightness" 3 | version = "1.0.0" 4 | authors = ["PabloMansanet "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "Define types bound by arbitrary invariants and conditions" 8 | repository = "https://github.com/PabloMansanet/tightness" 9 | readme = "README.md" 10 | 11 | [features] 12 | unsafe_access = [] 13 | 14 | [dependencies] 15 | paste = "1.0.5" 16 | thiserror = "1.0" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Pablo Mansanet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tightness 2 | 3 | This library provides a convenient way to define types bound by arbitrary 4 | conditions. 5 | 6 | ``` rust 7 | bound!(pub Letter: char where |c| c.is_alphabetic()); 8 | bound!(XorPair: (bool, bool) where |(a, b)| a ^ b); 9 | bound!(Username: String where |s| s.len() < 8); 10 | ``` 11 | 12 | The above defines three types (`Letter`, `XorPair` and `Username`) that are 13 | guaranteed to always fulfill the given conditions. This is enforced 14 | by checking the conditions on construction and after every mutation. 15 | 16 | Immutably, bounded types get out of the way and act as close as possible to the 17 | underlying type, implementing all traits that a typical `Newtype` wrapper would. 18 | 19 | Check out the [documentation](https://docs.rs/tightness/) for more details! 20 | 21 | Credit to Orson Peters (`orlp` in the Rust Discord) for the idea for the `Bound` 22 | trait, which was the seed from which this crate grew. 23 | 24 | # Caveat Emptor 25 | 26 | This crate offers a one-size-fits-all solution. This means it's probably a 27 | lot better to use specialized restricted types when possible (e.g. the standard 28 | library's `NonZeroUSize` type). These types will be further specialized and 29 | offer performance and size gains, implement more traits, and maximize the number 30 | of decisions made at compile time. 31 | 32 | If you're not particularly worried about the performance of checking the 33 | invariant after every mutation, there's no alternative in the ecosystem for the 34 | particular type restriction you want, or you just need something quick and 35 | convenient to protect your types, `tightness` may be for you! 36 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Borrow, 3 | fmt::Debug, 4 | marker::PhantomData, 5 | ops::{Deref, Index}, 6 | }; 7 | 8 | use thiserror::Error; 9 | 10 | #[derive(Error)] 11 | #[error("Value supplied did not satisfy the type invariant")] 12 | /// The result of a failed invariant check on construction. Contains the value 13 | /// that failed to uphold the invariant. 14 | pub struct ConstructionError(pub T); 15 | impl Debug for ConstructionError { 16 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 17 | f.debug_struct("ConstructionError").finish() 18 | } 19 | } 20 | 21 | /// The result of a failed invariant check at the end of mutation. 22 | /// 23 | /// In the cases where it's recoverable, this error contains the value 24 | /// that failed to uphold the invariant. 25 | #[derive(Error)] 26 | #[error("Value did not satisfy the type invariant after mutation")] 27 | pub struct MutationError(pub Option); 28 | impl Debug for MutationError { 29 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 30 | f.write_str("MutationError") 31 | } 32 | } 33 | 34 | /// The result of a broken invariant at some unspecified point in the 35 | /// past. This can only happen as a consequence of incorrect usage of 36 | /// `unsafe` accessors enabled with the `unsafe_access` flag 37 | #[cfg(feature = "unsafe_access")] 38 | #[derive(Error, Debug)] 39 | #[error("Invariant was broken at some point in the past")] 40 | pub struct BrokenInvariantError; 41 | 42 | /// Trait for an arbitrary condition that a bounded type must guarantee 43 | /// to uphold at all times. 44 | pub trait Bound { 45 | /// The type that the invariant is predicated on. 46 | type Target; 47 | /// The condition that the target type must verify at all times. 48 | fn check(target: &Self::Target) -> bool; 49 | } 50 | 51 | /// A bounded type, i.e. a thin wrapper around an inner type that guarantees a 52 | /// specific invariant is always held. Generic over an inner type `T` and a 53 | /// [`Bound`](Bound) that targets it. 54 | /// 55 | /// Bounded types can be constructed directly or through the [`bound`](bound) 56 | /// macro: 57 | /// ``` 58 | /// // Defined directly 59 | /// use tightness::{Bounded, Bound}; 60 | /// 61 | /// #[derive(Debug)] 62 | /// pub struct LetterBound; 63 | /// 64 | /// impl tightness::Bound for LetterBound { 65 | /// type Target = char; 66 | /// fn check(target: &char) -> bool { target.is_alphabetic() } 67 | /// } 68 | /// 69 | /// pub type Letter = tightness::Bounded; 70 | /// 71 | /// ``` 72 | /// 73 | /// ``` 74 | /// // Defined via macro 75 | /// use tightness::{bound, Bounded}; 76 | /// bound!(pub Letter: char where |l| l.is_alphabetic()); 77 | /// ``` 78 | #[derive(Debug)] 79 | pub struct Bounded>(T, PhantomData); 80 | 81 | impl> Bounded { 82 | /// Fallible constructor. Will return an error if the argument `t` 83 | /// doesn't fulfill the conditions of the bound. 84 | /// 85 | /// ``` 86 | /// # use tightness::{bound, Bounded, ConstructionError}; 87 | /// bound!(Letter: char where |c| c.is_alphabetic()); 88 | /// assert!(Letter::new('a').is_ok()); 89 | /// assert!(matches!(Letter::new('5'), Err(ConstructionError('5')))); 90 | /// ``` 91 | pub fn new(t: T) -> Result> { 92 | if B::check(&t) { 93 | Ok(Self(t, Default::default())) 94 | } else { 95 | Err(ConstructionError(t)) 96 | } 97 | } 98 | 99 | /// Will panic if the conditions of the bound don't hold after mutation. 100 | /// 101 | /// ```should_panic 102 | /// # use tightness::{bound, Bounded}; 103 | /// bound!(Letter: char where |c| c.is_alphabetic()); 104 | /// let mut letter = Letter::new('a').unwrap(); 105 | /// letter.mutate(|l| *l = 'b'); 106 | /// 107 | /// // Panics: 108 | /// letter.mutate(|l| *l = '5'); 109 | /// ``` 110 | pub fn mutate(&mut self, f: impl FnOnce(&mut T)) { 111 | f(&mut self.0); 112 | assert!(B::check(&self.0)); 113 | } 114 | 115 | /// If the conditions of the bound don't hold after mutation, will restore to a given value. 116 | /// 117 | /// ``` 118 | /// # use tightness::{bound, Bounded}; 119 | /// bound!(Letter: char where |c| c.is_alphabetic()); 120 | /// let mut letter = Letter::new('a').unwrap(); 121 | /// let mut fallback = Letter::new('b').unwrap(); 122 | /// 123 | /// letter.mutate_or(fallback, |l| *l = '5').unwrap_err(); 124 | /// assert_eq!(*letter, 'b'); 125 | /// ``` 126 | pub fn mutate_or( 127 | &mut self, 128 | default: Self, 129 | f: impl FnOnce(&mut T), 130 | ) -> Result<(), MutationError> { 131 | f(&mut self.0); 132 | if B::check(&self.0) { 133 | Ok(()) 134 | } else { 135 | *self = default; 136 | Err(MutationError(None)) 137 | } 138 | } 139 | 140 | /// The value is dropped if the conditions of the bound don't hold after mutation. 141 | /// ``` 142 | /// # use tightness::{bound, Bounded, MutationError}; 143 | /// bound!(Letter: char where |c| c.is_alphabetic()); 144 | /// let mut letter = Letter::new('a').unwrap(); 145 | /// 146 | /// let letter = letter.into_mutated(|l| *l = 'b').unwrap(); 147 | /// let result = letter.into_mutated(|l| *l = '5'); 148 | /// 149 | /// assert!(matches!(result, Err(MutationError(Some('5'))))); 150 | /// ``` 151 | pub fn into_mutated(mut self, f: impl FnOnce(&mut T)) -> Result> { 152 | f(&mut self.0); 153 | if B::check(&self.0) { 154 | Ok(self) 155 | } else { 156 | Err(MutationError(Some(self.0))) 157 | } 158 | } 159 | 160 | /// Access the inner value through an immutable reference. 161 | pub fn get(&self) -> &T { &self.0 } 162 | 163 | /// Retrieve the inner, unprotected value. 164 | pub fn into_inner(self) -> T { self.0 } 165 | 166 | /// Invariant must be upheld manually! 167 | #[cfg(feature = "unsafe_access")] 168 | pub unsafe fn new_unchecked(t: T) -> Self { Self(t, Default::default()) } 169 | 170 | /// Invariant must be upheld manually! 171 | #[cfg(feature = "unsafe_access")] 172 | pub unsafe fn mutate_unchecked(&mut self, f: impl FnOnce(&mut T)) { f(&mut self.0) } 173 | 174 | /// Gives mutable access to the internals without upholding invariants. 175 | /// They must continue to be upheld manually while the reference lives! 176 | #[cfg(feature = "unsafe_access")] 177 | pub unsafe fn get_mut(&mut self) -> &mut T { &mut self.0 } 178 | 179 | /// Verifies invariants. This is guaranteed to succeed unless you've used 180 | /// one of the `unsafe` methods that require variants to be manually upheld. 181 | #[cfg(feature = "unsafe_access")] 182 | pub fn verify(&self) -> Result<(), BrokenInvariantError> { 183 | if B::check(&self.0) { 184 | Ok(()) 185 | } else { 186 | Err(BrokenInvariantError) 187 | } 188 | } 189 | } 190 | 191 | impl> Bounded { 192 | /// Preserves invariants after mutation, erroring out if the attempt to mutate was 193 | /// invalid. Requires a copy to ensure the value is recoverable. 194 | pub fn try_mutate(&mut self, f: impl FnOnce(&mut T)) -> Result<(), MutationError> { 195 | let mut duplicate = self.0.clone(); 196 | f(&mut duplicate); 197 | if B::check(&duplicate) { 198 | self.0 = duplicate; 199 | Ok(()) 200 | } else { 201 | Err(MutationError(None)) 202 | } 203 | } 204 | } 205 | 206 | impl> Clone for Bounded { 207 | fn clone(&self) -> Self { Self(self.0.clone(), Default::default()) } 208 | } 209 | 210 | impl> Borrow for Bounded { 211 | fn borrow(&self) -> &T { &self.0 } 212 | } 213 | 214 | impl> AsRef for Bounded { 215 | fn as_ref(&self) -> &T { &self.0 } 216 | } 217 | 218 | impl> Deref for Bounded { 219 | type Target = T; 220 | fn deref(&self) -> &Self::Target { &self.0 } 221 | } 222 | 223 | impl> PartialEq for Bounded { 224 | fn eq(&self, other: &Self) -> bool { self.0.eq(&other.0) } 225 | } 226 | 227 | impl> Eq for Bounded {} 228 | 229 | impl> PartialOrd for Bounded { 230 | fn partial_cmp(&self, other: &Self) -> Option { 231 | self.0.partial_cmp(&other.0) 232 | } 233 | } 234 | 235 | impl> Ord for Bounded { 236 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.0.cmp(&other.0) } 237 | } 238 | 239 | impl> Copy for Bounded {} 240 | impl> core::hash::Hash for Bounded { 241 | fn hash(&self, state: &mut H) { self.0.hash(state) } 242 | } 243 | 244 | impl, U, B: Bound> Index for Bounded { 245 | type Output = T::Output; 246 | 247 | fn index(&self, index: U) -> &Self::Output { self.0.index(index) } 248 | } 249 | 250 | #[cfg(test)] 251 | mod tests { 252 | #[derive(Debug)] 253 | struct IsPositive; 254 | impl Bound for IsPositive { 255 | type Target = i32; 256 | fn check(x: &i32) -> bool { *x >= 0 } 257 | } 258 | 259 | use super::*; 260 | #[test] 261 | fn constructing_with_passing_bounds_succeeds() { Bounded::::new(1).unwrap(); } 262 | 263 | #[test] 264 | fn constructing_with_failing_bounds_fails() { 265 | assert!(Bounded::::new(-5).is_err()); 266 | } 267 | 268 | #[test] 269 | fn mutating_with_passing_bounds_succeeds() { 270 | let mut bounded = Bounded::::new(5i32).unwrap(); 271 | bounded.mutate(|i| *i = 2 * *i); 272 | } 273 | 274 | #[test] 275 | #[should_panic] 276 | fn mutating_with_failing_bounds_panics() { 277 | let mut bounded = Bounded::::new(5i32).unwrap(); 278 | bounded.mutate(|i| *i = -5); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/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`](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; 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))] 101 | pub use crate::core::*; 102 | mod core; 103 | 104 | #[doc(hidden)] 105 | pub 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`](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] 123 | macro_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)] 143 | mod 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 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) {} 201 | let month = Month::new(1).unwrap(); 202 | takes_as_ref(&month); 203 | assert_eq!(month, month.clone()); 204 | 205 | bound!(FixedVec: Vec where |v| v.len() == 4); 206 | let fixed = FixedVec::new(vec![false, true, false, false]).unwrap(); 207 | assert_eq!(fixed[1], true); 208 | } 209 | } 210 | --------------------------------------------------------------------------------