textual/validation/
mod.rs1use std::sync::Arc;
2
3#[derive(Debug, Clone, Default)]
4pub struct ValidationResult {
5 pub is_valid: bool,
6 pub failure_descriptions: Vec<String>,
7}
8
9impl ValidationResult {
10 pub fn success() -> Self {
11 Self {
12 is_valid: true,
13 failure_descriptions: Vec::new(),
14 }
15 }
16
17 pub fn failure(message: impl Into<String>) -> Self {
18 Self {
19 is_valid: false,
20 failure_descriptions: vec![message.into()],
21 }
22 }
23}
24
25pub trait Validator {
26 fn validate(&self, value: &str) -> ValidationResult;
27}
28
29pub type ValidatorRef = Arc<dyn Validator + Send + Sync>;
30
31pub struct Function {
32 predicate: Arc<dyn Fn(&str) -> bool + Send + Sync>,
33 message: String,
34}
35
36impl Function {
37 pub fn new(
38 predicate: impl Fn(&str) -> bool + Send + Sync + 'static,
39 message: impl Into<String>,
40 ) -> Self {
41 Self {
42 predicate: Arc::new(predicate),
43 message: message.into(),
44 }
45 }
46}
47
48impl Validator for Function {
49 fn validate(&self, value: &str) -> ValidationResult {
50 if (self.predicate)(value) {
51 ValidationResult::success()
52 } else {
53 ValidationResult::failure(self.message.clone())
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy)]
59pub struct Number {
60 minimum: Option<f64>,
61 maximum: Option<f64>,
62}
63
64impl Default for Number {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl Number {
71 pub fn new() -> Self {
72 Self {
73 minimum: None,
74 maximum: None,
75 }
76 }
77
78 pub fn minimum(mut self, value: f64) -> Self {
79 self.minimum = Some(value);
80 self
81 }
82
83 pub fn maximum(mut self, value: f64) -> Self {
84 self.maximum = Some(value);
85 self
86 }
87}
88
89impl Validator for Number {
90 fn validate(&self, value: &str) -> ValidationResult {
91 let value = value.trim();
92 if value.is_empty() {
93 return ValidationResult::failure("Value is required.");
94 }
95
96 let Ok(num) = value.parse::<f64>() else {
97 return ValidationResult::failure("Value is not a number.");
98 };
99
100 if num.is_nan() || num.is_infinite() {
101 return ValidationResult::failure("Value is not a number.");
102 }
103
104 if let Some(min) = self.minimum {
105 if num < min {
106 return ValidationResult::failure(format!("Value must be >= {min}."));
107 }
108 }
109 if let Some(max) = self.maximum {
110 if num > max {
111 return ValidationResult::failure(format!("Value must be <= {max}."));
112 }
113 }
114
115 ValidationResult::success()
116 }
117}
118
119#[derive(Debug, Clone, Copy)]
124pub struct Integer {
125 minimum: Option<i64>,
126 maximum: Option<i64>,
127}
128
129impl Default for Integer {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135impl Integer {
136 pub fn new() -> Self {
137 Self {
138 minimum: None,
139 maximum: None,
140 }
141 }
142
143 pub fn minimum(mut self, value: i64) -> Self {
144 self.minimum = Some(value);
145 self
146 }
147
148 pub fn maximum(mut self, value: i64) -> Self {
149 self.maximum = Some(value);
150 self
151 }
152}
153
154impl Validator for Integer {
155 fn validate(&self, value: &str) -> ValidationResult {
156 let value = value.trim();
157 if value.is_empty() {
158 return ValidationResult::failure("Must be a valid integer.");
159 }
160
161 let Ok(num) = value.parse::<i64>() else {
162 return ValidationResult::failure("Must be a valid integer.");
163 };
164
165 if let Some(min) = self.minimum {
166 if num < min {
167 return ValidationResult::failure(format!(
168 "Must be greater than or equal to {min}."
169 ));
170 }
171 }
172 if let Some(max) = self.maximum {
173 if num > max {
174 return ValidationResult::failure(format!("Must be less than or equal to {max}."));
175 }
176 }
177
178 ValidationResult::success()
179 }
180}
181
182#[derive(Debug, Clone, Copy)]
186pub struct Length {
187 minimum: Option<usize>,
188 maximum: Option<usize>,
189}
190
191impl Default for Length {
192 fn default() -> Self {
193 Self::new()
194 }
195}
196
197impl Length {
198 pub fn new() -> Self {
199 Self {
200 minimum: None,
201 maximum: None,
202 }
203 }
204
205 pub fn minimum(mut self, value: usize) -> Self {
206 self.minimum = Some(value);
207 self
208 }
209
210 pub fn maximum(mut self, value: usize) -> Self {
211 self.maximum = Some(value);
212 self
213 }
214}
215
216impl Validator for Length {
217 fn validate(&self, value: &str) -> ValidationResult {
218 let len = value.len();
219 let too_short = self.minimum.is_some_and(|min| len < min);
220 let too_long = self.maximum.is_some_and(|max| len > max);
221
222 if too_short || too_long {
223 let msg = match (self.minimum, self.maximum) {
224 (Some(min), None) => format!("Must be longer than {min} characters."),
225 (None, Some(max)) => format!("Must be shorter than {max} characters."),
226 (Some(min), Some(max)) => format!("Must be between {min} and {max} characters."),
227 _ => "Invalid length.".to_string(),
228 };
229 return ValidationResult::failure(msg);
230 }
231
232 ValidationResult::success()
233 }
234}
235
236#[derive(Debug, Clone, Copy)]
241pub struct Url;
242
243impl Default for Url {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249impl Url {
250 pub fn new() -> Self {
251 Self
252 }
253}
254
255impl Validator for Url {
256 fn validate(&self, value: &str) -> ValidationResult {
257 let value = value.trim();
258 let Some((scheme, rest)) = value.split_once("://") else {
260 return ValidationResult::failure("Must be a valid URL.");
261 };
262 if scheme.is_empty() {
263 return ValidationResult::failure("Must be a valid URL.");
264 }
265 let netloc = rest.split(&['/', '?', '#'][..]).next().unwrap_or("");
267 if netloc.is_empty() {
268 return ValidationResult::failure("Must be a valid URL.");
269 }
270 ValidationResult::success()
271 }
272}
273
274pub struct Regex {
279 pattern: regex::Regex,
280}
281
282impl Regex {
283 pub fn new(pattern: regex::Regex) -> Self {
284 Self { pattern }
285 }
286
287 pub fn compile(pattern: &str) -> Self {
289 Self {
290 pattern: regex::Regex::new(pattern).expect("invalid regex pattern"),
291 }
292 }
293
294 pub fn try_compile(pattern: &str) -> Option<Self> {
296 regex::Regex::new(pattern).ok().map(|r| Self { pattern: r })
297 }
298}
299
300impl std::fmt::Debug for Regex {
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 f.debug_struct("Regex")
303 .field("pattern", &self.pattern.as_str())
304 .finish()
305 }
306}
307
308impl Validator for Regex {
309 fn validate(&self, value: &str) -> ValidationResult {
310 if self
312 .pattern
313 .find(value)
314 .is_some_and(|m| m.start() == 0 && m.end() == value.len())
315 {
316 ValidationResult::success()
317 } else {
318 ValidationResult::failure(format!(
319 "Must match regular expression {:?}.",
320 self.pattern.as_str()
321 ))
322 }
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn integer_valid() {
332 let v = Integer::new();
333 assert!(v.validate("42").is_valid);
334 assert!(v.validate("-7").is_valid);
335 assert!(v.validate("0").is_valid);
336 }
337
338 #[test]
339 fn integer_invalid() {
340 let v = Integer::new();
341 assert!(!v.validate("3.5").is_valid);
342 assert!(!v.validate("abc").is_valid);
343 assert!(!v.validate("").is_valid);
344 }
345
346 #[test]
347 fn integer_range() {
348 let v = Integer::new().minimum(0).maximum(100);
349 assert!(v.validate("50").is_valid);
350 assert!(!v.validate("-1").is_valid);
351 assert!(!v.validate("101").is_valid);
352 }
353
354 #[test]
355 fn length_valid() {
356 let v = Length::new().minimum(2).maximum(5);
357 assert!(v.validate("ab").is_valid);
358 assert!(v.validate("abcde").is_valid);
359 }
360
361 #[test]
362 fn length_invalid() {
363 let v = Length::new().minimum(2).maximum(5);
364 assert!(!v.validate("a").is_valid);
365 assert!(!v.validate("abcdef").is_valid);
366 }
367
368 #[test]
369 fn length_unbounded_min() {
370 let v = Length::new().maximum(3);
371 assert!(v.validate("").is_valid);
372 assert!(v.validate("abc").is_valid);
373 assert!(!v.validate("abcd").is_valid);
374 }
375
376 #[test]
377 fn length_unbounded_max() {
378 let v = Length::new().minimum(2);
379 assert!(!v.validate("a").is_valid);
380 assert!(v.validate("ab").is_valid);
381 assert!(v.validate("abcdef").is_valid);
382 }
383
384 #[test]
385 fn url_valid() {
386 let v = Url::new();
387 assert!(v.validate("https://example.com").is_valid);
388 assert!(v.validate("http://foo.bar/path").is_valid);
389 assert!(v.validate("ftp://files.example.com").is_valid);
390 }
391
392 #[test]
393 fn url_invalid() {
394 let v = Url::new();
395 assert!(!v.validate("not-a-url").is_valid);
396 assert!(!v.validate("://missing-scheme").is_valid);
397 assert!(!v.validate("http://").is_valid);
398 assert!(!v.validate("").is_valid);
399 }
400
401 #[test]
402 fn regex_valid() {
403 let v = Regex::compile(r"\d{3}-\d{4}");
404 assert!(v.validate("123-4567").is_valid);
405 }
406
407 #[test]
408 fn regex_invalid() {
409 let v = Regex::compile(r"\d{3}-\d{4}");
410 assert!(!v.validate("12-4567").is_valid);
411 assert!(!v.validate("abc").is_valid);
412 }
413
414 #[test]
415 fn regex_fullmatch_semantics() {
416 let v = Regex::compile(r"\d+");
418 assert!(v.validate("123").is_valid);
419 assert!(!v.validate("123abc").is_valid);
420 assert!(!v.validate("abc123").is_valid);
421 }
422
423 #[test]
424 fn regex_try_compile_invalid() {
425 assert!(Regex::try_compile("[invalid").is_none());
426 }
427
428 #[test]
429 fn number_rejects_nan_inf() {
430 let v = Number::new();
431 assert!(!v.validate("NaN").is_valid);
432 assert!(!v.validate("inf").is_valid);
433 assert!(!v.validate("-inf").is_valid);
434 assert!(!v.validate("infinity").is_valid);
435 }
436}