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