twine_core/constraint.rs
1//! Type-level numeric constraints with zero runtime cost.
2//!
3//! This module lets you express numeric constraints like "non-negative",
4//! "non-zero", or "strictly positive" at the type level, with zero runtime
5//! overhead after construction.
6//!
7//! With these types, your APIs and components can trust that values always
8//! satisfy the required numeric constraints.
9//! This guarantee leads to designs that are both safer and more self-documenting.
10//!
11//! # Provided Constraints
12//!
13//! The following marker types represent the most common numeric invariants:
14//!
15//! - [`NonNegative`]: Zero or greater
16//! - [`NonPositive`]: Zero or less
17//! - [`NonZero`]: Not equal to zero
18//! - [`StrictlyNegative`]: Less than zero
19//! - [`StrictlyPositive`]: Greater than zero
20//!
21//! Each marker can be used with the generic [`Constrained<T, C>`] wrapper,
22//! where `C` is the marker type implementing [`Constraint<T>`].
23//! Each also provides an associated [`new()`] constructor for convenience.
24//!
25//! See the documentation and tests for each constraint for more usage patterns.
26//!
27//! # Extending
28//!
29//! You can define custom numeric invariants by implementing [`Constraint<T>`]
30//! for your own zero-sized marker types.
31
32mod non_negative;
33mod non_positive;
34mod non_zero;
35mod strictly_negative;
36mod strictly_positive;
37
38use std::{iter::Sum, marker::PhantomData, ops::Add};
39
40use num_traits::Zero;
41use thiserror::Error;
42
43pub use non_negative::NonNegative;
44pub use non_positive::NonPositive;
45pub use non_zero::NonZero;
46pub use strictly_negative::StrictlyNegative;
47pub use strictly_positive::StrictlyPositive;
48
49/// A trait for enforcing numeric invariants at construction time.
50///
51/// Implement this trait for any marker type representing a numeric constraint,
52/// such as [`NonNegative`] or [`StrictlyPositive`].
53pub trait Constraint<T> {
54 /// Checks that the given value satisfies this constraint.
55 ///
56 /// # Errors
57 ///
58 /// Returns a [`ConstraintError`] if the value does not satisfy the constraint.
59 fn check(value: &T) -> Result<(), ConstraintError>;
60}
61
62/// An error returned when a [`Constraint`] is violated.
63///
64/// This enum is marked `#[non_exhaustive]` and may include additional variants
65/// in future releases.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
67#[non_exhaustive]
68pub enum ConstraintError {
69 #[error("value must not be negative")]
70 Negative,
71
72 #[error("value must not be positive")]
73 Positive,
74
75 #[error("value must not be zero")]
76 Zero,
77
78 #[error("value is not a number")]
79 NotANumber,
80}
81
82/// A wrapper enforcing a numeric constraint at construction time.
83///
84/// Combine this with one of the provided marker types (such as [`NonNegative`])
85/// or your own [`Constraint<T>`] implementation.
86///
87/// See the [module documentation](crate::constraint) for details and usage patterns.
88///
89/// # Example
90///
91/// ```
92/// use twine_core::constraint::{Constrained, StrictlyPositive};
93///
94/// let n = Constrained::<_, StrictlyPositive>::new(42).unwrap();
95/// assert_eq!(n.into_inner(), 42);
96/// ```
97#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
98pub struct Constrained<T, C: Constraint<T>> {
99 value: T,
100 _marker: PhantomData<C>,
101}
102
103impl<T, C: Constraint<T>> Constrained<T, C> {
104 /// Constructs a new constrained value.
105 ///
106 /// # Errors
107 ///
108 /// Returns an error if the value does not satisfy the constraint.
109 pub fn new(value: T) -> Result<Self, ConstraintError> {
110 C::check(&value)?;
111 Ok(Self {
112 value,
113 _marker: PhantomData,
114 })
115 }
116
117 /// Consumes the wrapper and returns the inner value.
118 pub fn into_inner(self) -> T {
119 self.value
120 }
121}
122
123/// Returns a reference to the inner unconstrained value.
124impl<T, C: Constraint<T>> AsRef<T> for Constrained<T, C> {
125 fn as_ref(&self) -> &T {
126 &self.value
127 }
128}
129
130/// Sums constrained values for which addition is valid.
131///
132/// Applies to all constraints that are preserved under addition.
133impl<T, C> Sum for Constrained<T, C>
134where
135 C: Constraint<T>,
136 Constrained<T, C>: Add<Output = Self> + Zero,
137{
138 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
139 iter.fold(Self::zero(), |a, b| a + b)
140 }
141}