type_lib/rules/numeric.rs
1//! Numeric rules: sign checks and inclusive integer ranges.
2//!
3//! Sign rules ([`Positive`], [`NonNegative`], [`Negative`], [`NonPositive`])
4//! apply to the signed integer and floating-point primitives. [`InRange`] applies
5//! to the integer primitives that fit losslessly in an `i64`
6//! (`i8`/`i16`/`i32`/`i64`, `u8`/`u16`/`u32`).
7
8use crate::{ValidationError, Validator};
9
10/// Accepts strictly positive numbers (`value > 0`).
11///
12/// # Examples
13///
14/// ```rust
15/// use type_lib::rules::Positive;
16/// use type_lib::Validator;
17///
18/// assert!(Positive::validate(&3).is_ok());
19/// assert!(Positive::validate(&0).is_err());
20/// assert!(Positive::validate(&-1.5_f64).is_err());
21/// ```
22pub struct Positive;
23
24/// Accepts non-negative numbers (`value >= 0`).
25///
26/// # Examples
27///
28/// ```rust
29/// use type_lib::rules::NonNegative;
30/// use type_lib::Validator;
31///
32/// assert!(NonNegative::validate(&0).is_ok());
33/// assert!(NonNegative::validate(&-1).is_err());
34/// ```
35pub struct NonNegative;
36
37/// Accepts strictly negative numbers (`value < 0`).
38///
39/// # Examples
40///
41/// ```rust
42/// use type_lib::rules::Negative;
43/// use type_lib::Validator;
44///
45/// assert!(Negative::validate(&-2).is_ok());
46/// assert!(Negative::validate(&0).is_err());
47/// ```
48pub struct Negative;
49
50/// Accepts non-positive numbers (`value <= 0`).
51///
52/// # Examples
53///
54/// ```rust
55/// use type_lib::rules::NonPositive;
56/// use type_lib::Validator;
57///
58/// assert!(NonPositive::validate(&0).is_ok());
59/// assert!(NonPositive::validate(&1).is_err());
60/// ```
61pub struct NonPositive;
62
63macro_rules! impl_sign_rules {
64 ($($t:ty),* $(,)?) => {
65 $(
66 impl Validator<$t> for Positive {
67 type Error = ValidationError;
68
69 fn validate(value: &$t) -> Result<(), Self::Error> {
70 if *value > (0 as $t) {
71 Ok(())
72 } else {
73 Err(ValidationError::new("positive", "value must be greater than zero"))
74 }
75 }
76 }
77
78 impl Validator<$t> for NonNegative {
79 type Error = ValidationError;
80
81 fn validate(value: &$t) -> Result<(), Self::Error> {
82 if *value >= (0 as $t) {
83 Ok(())
84 } else {
85 Err(ValidationError::new("non_negative", "value must not be negative"))
86 }
87 }
88 }
89
90 impl Validator<$t> for Negative {
91 type Error = ValidationError;
92
93 fn validate(value: &$t) -> Result<(), Self::Error> {
94 if *value < (0 as $t) {
95 Ok(())
96 } else {
97 Err(ValidationError::new("negative", "value must be less than zero"))
98 }
99 }
100 }
101
102 impl Validator<$t> for NonPositive {
103 type Error = ValidationError;
104
105 fn validate(value: &$t) -> Result<(), Self::Error> {
106 if *value <= (0 as $t) {
107 Ok(())
108 } else {
109 Err(ValidationError::new("non_positive", "value must not be positive"))
110 }
111 }
112 }
113 )*
114 };
115}
116
117impl_sign_rules!(i8, i16, i32, i64, i128, isize, f32, f64);
118
119/// Accepts integers within the inclusive range `MIN..=MAX`.
120///
121/// Implemented for the integer types that convert losslessly to `i64`
122/// (`i8`/`i16`/`i32`/`i64`, `u8`/`u16`/`u32`). The bounds are `i64` const
123/// generics, so the rule covers any range expressible in an `i64`.
124///
125/// # Examples
126///
127/// ```rust
128/// use type_lib::rules::InRange;
129/// use type_lib::Validator;
130///
131/// // A percentage: 0 to 100 inclusive.
132/// assert!(InRange::<0, 100>::validate(&50_u8).is_ok());
133/// assert!(InRange::<0, 100>::validate(&150_i32).is_err());
134///
135/// // Ranges may be negative.
136/// assert!(InRange::<-10, 10>::validate(&-5_i16).is_ok());
137/// ```
138pub struct InRange<const MIN: i64, const MAX: i64>;
139
140macro_rules! impl_in_range {
141 ($($t:ty),* $(,)?) => {
142 $(
143 impl<const MIN: i64, const MAX: i64> Validator<$t> for InRange<MIN, MAX> {
144 type Error = ValidationError;
145
146 fn validate(value: &$t) -> Result<(), Self::Error> {
147 let value = i64::from(*value);
148 if value >= MIN && value <= MAX {
149 Ok(())
150 } else {
151 Err(ValidationError::new("in_range", "value is out of range"))
152 }
153 }
154 }
155 )*
156 };
157}
158
159impl_in_range!(i8, i16, i32, i64, u8, u16, u32);
160
161#[cfg(test)]
162mod tests {
163 #![allow(clippy::unwrap_used, clippy::expect_used)]
164
165 use super::*;
166
167 #[test]
168 fn sign_rules_on_integers() {
169 assert!(Positive::validate(&1_i32).is_ok());
170 assert!(Positive::validate(&0_i32).is_err());
171 assert!(NonNegative::validate(&0_i32).is_ok());
172 assert!(NonNegative::validate(&-1_i32).is_err());
173 assert!(Negative::validate(&-1_i32).is_ok());
174 assert!(Negative::validate(&0_i32).is_err());
175 assert!(NonPositive::validate(&0_i32).is_ok());
176 assert!(NonPositive::validate(&1_i32).is_err());
177 }
178
179 #[test]
180 fn sign_rules_on_floats() {
181 assert!(Positive::validate(&0.5_f64).is_ok());
182 assert!(Positive::validate(&0.0_f64).is_err());
183 assert!(Negative::validate(&-0.5_f32).is_ok());
184 }
185
186 #[test]
187 fn in_range_inclusive_bounds_and_codes() {
188 assert!(InRange::<0, 100>::validate(&0_u8).is_ok());
189 assert!(InRange::<0, 100>::validate(&100_u8).is_ok());
190 assert_eq!(
191 InRange::<0, 100>::validate(&101_i32).unwrap_err().code(),
192 "in_range",
193 );
194 assert!(InRange::<-5, 5>::validate(&-5_i16).is_ok());
195 assert!(InRange::<-5, 5>::validate(&-6_i16).is_err());
196 }
197}