Skip to main content

catfive/
number.rs

1use crate::error::ValidationError;
2use crate::format_helper::format_template;
3use crate::traits::Validator;
4use std::fmt::Display;
5use std::sync::Arc;
6
7type NumberRule<T> = Arc<dyn Fn(&T) -> Option<ValidationError> + Send + Sync>;
8
9/// Fluent validator for numeric values.
10#[derive(Clone)]
11pub struct NumberValidator<T> {
12    rules: Vec<NumberRule<T>>,
13}
14
15impl<T> Default for NumberValidator<T> {
16    fn default() -> Self {
17        Self { rules: Vec::new() }
18    }
19}
20
21impl<T> NumberValidator<T>
22where
23    T: PartialOrd + Copy + Display + Send + Sync + 'static,
24{
25    /// Creates a new empty numeric validator.
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    /// Enforces a minimum numeric value.
31    pub fn min(self, minimum: T) -> Self {
32        self.min_msg(minimum, "Must be at least {min}, got {value}")
33    }
34
35    /// Enforces a minimum numeric value with a custom message template.
36    ///
37    /// Available placeholders: `{min}`, `{value}`.
38    pub fn min_msg(mut self, minimum: T, msg_template: &str) -> Self {
39        let tpl = msg_template.to_string();
40        self.rules.push(Arc::new(move |val| {
41            if *val < minimum {
42                let msg = format_template(
43                    &tpl,
44                    &[("min", &minimum.to_string()), ("value", &val.to_string())],
45                );
46                Some(ValidationError::new("min_value", msg))
47            } else {
48                None
49            }
50        }));
51        self
52    }
53
54    /// Enforces a maximum numeric value.
55    pub fn max(self, maximum: T) -> Self {
56        self.max_msg(maximum, "Must not exceed {max}, got {value}")
57    }
58
59    /// Enforces a maximum numeric value with a custom message template.
60    ///
61    /// Available placeholders: `{max}`, `{value}`.
62    pub fn max_msg(mut self, maximum: T, msg_template: &str) -> Self {
63        let tpl = msg_template.to_string();
64        self.rules.push(Arc::new(move |val| {
65            if *val > maximum {
66                let msg = format_template(
67                    &tpl,
68                    &[("max", &maximum.to_string()), ("value", &val.to_string())],
69                );
70                Some(ValidationError::new("max_value", msg))
71            } else {
72                None
73            }
74        }));
75        self
76    }
77
78    /// Enforces that the value is within a given range (inclusive).
79    pub fn range(self, min_val: T, max_val: T) -> Self {
80        self.range_msg(
81            min_val,
82            max_val,
83            "Must be between {min} and {max}, got {value}",
84        )
85    }
86
87    /// Enforces that the value is within a given range with a custom message template.
88    ///
89    /// Available placeholders: `{min}`, `{max}`, `{value}`.
90    pub fn range_msg(mut self, min_val: T, max_val: T, msg_template: &str) -> Self {
91        let tpl = msg_template.to_string();
92        self.rules.push(Arc::new(move |val| {
93            if *val < min_val || *val > max_val {
94                let msg = format_template(
95                    &tpl,
96                    &[
97                        ("min", &min_val.to_string()),
98                        ("max", &max_val.to_string()),
99                        ("value", &val.to_string()),
100                    ],
101                );
102                Some(ValidationError::new("range", msg))
103            } else {
104                None
105            }
106        }));
107        self
108    }
109
110    /// Adds a custom user-defined validation closure for numeric types.
111    pub fn custom<F>(mut self, validator: F) -> Self
112    where
113        F: Fn(&T) -> Result<(), ValidationError> + Send + Sync + 'static,
114    {
115        self.rules.push(Arc::new(move |val| validator(val).err()));
116        self
117    }
118}
119
120impl<T> NumberValidator<T>
121where
122    T: PartialOrd + Default + Copy + Display + Send + Sync + 'static,
123{
124    /// Enforces that the numeric value is strictly positive (> 0).
125    pub fn positive(self) -> Self {
126        self.positive_msg("Must be greater than 0, got {value}")
127    }
128
129    /// Enforces that the numeric value is strictly positive with a custom message template.
130    ///
131    /// Available placeholders: `{value}`.
132    pub fn positive_msg(mut self, msg_template: &str) -> Self {
133        let zero = T::default();
134        let tpl = msg_template.to_string();
135        self.rules.push(Arc::new(move |val| {
136            if *val <= zero {
137                let msg = format_template(&tpl, &[("value", &val.to_string())]);
138                Some(ValidationError::new("positive", msg))
139            } else {
140                None
141            }
142        }));
143        self
144    }
145
146    /// Enforces that the numeric value is strictly negative (< 0).
147    pub fn negative(self) -> Self {
148        self.negative_msg("Must be less than 0, got {value}")
149    }
150
151    /// Enforces that the numeric value is strictly negative with a custom message template.
152    ///
153    /// Available placeholders: `{value}`.
154    pub fn negative_msg(mut self, msg_template: &str) -> Self {
155        let zero = T::default();
156        let tpl = msg_template.to_string();
157        self.rules.push(Arc::new(move |val| {
158            if *val >= zero {
159                let msg = format_template(&tpl, &[("value", &val.to_string())]);
160                Some(ValidationError::new("negative", msg))
161            } else {
162                None
163            }
164        }));
165        self
166    }
167}
168
169impl<T: Send + Sync> Validator<T> for NumberValidator<T> {
170    fn validate_all(&self, value: &T) -> Vec<ValidationError> {
171        let mut errors = Vec::new();
172        for rule in &self.rules {
173            if let Some(err) = rule(value) {
174                errors.push(err);
175            }
176        }
177        errors
178    }
179}