rust_zero_core/
validation.rs1use serde::Serialize;
2use std::{fmt, ops::RangeInclusive};
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
6pub struct Violation {
7 pub field: String,
8 pub code: &'static str,
9 pub message: String,
10}
11
12#[derive(Debug, Default)]
14pub struct Validation {
15 violations: Vec<Violation>,
16}
17
18impl Validation {
19 pub fn new() -> Self {
20 Self::default()
21 }
22
23 pub fn check(
24 &mut self,
25 field: impl Into<String>,
26 condition: bool,
27 code: &'static str,
28 message: impl Into<String>,
29 ) -> &mut Self {
30 if !condition {
31 self.violations.push(Violation {
32 field: field.into(),
33 code,
34 message: message.into(),
35 });
36 }
37 self
38 }
39
40 pub fn required(&mut self, field: impl Into<String>, value: &str) -> &mut Self {
41 self.check(
42 field,
43 !value.trim().is_empty(),
44 "required",
45 "must not be empty",
46 )
47 }
48
49 pub fn length(
50 &mut self,
51 field: impl Into<String>,
52 value: &str,
53 range: RangeInclusive<usize>,
54 ) -> &mut Self {
55 let length = value.chars().count();
56 let message = format!(
57 "length must be between {} and {}",
58 range.start(),
59 range.end()
60 );
61 self.check(field, range.contains(&length), "length", message)
62 }
63
64 pub fn range<T>(
65 &mut self,
66 field: impl Into<String>,
67 value: T,
68 range: RangeInclusive<T>,
69 ) -> &mut Self
70 where
71 T: PartialOrd + fmt::Display,
72 {
73 let message = format!("must be between {} and {}", range.start(), range.end());
74 self.check(field, range.contains(&value), "range", message)
75 }
76
77 pub fn one_of<T>(&mut self, field: impl Into<String>, value: &T, allowed: &[T]) -> &mut Self
78 where
79 T: PartialEq + fmt::Display,
80 {
81 self.check(
82 field,
83 allowed.contains(value),
84 "one_of",
85 format!(
86 "must be one of [{}]",
87 allowed
88 .iter()
89 .map(ToString::to_string)
90 .collect::<Vec<_>>()
91 .join(", ")
92 ),
93 )
94 }
95
96 pub fn finish(self) -> Result<(), ValidationErrors> {
97 if self.violations.is_empty() {
98 Ok(())
99 } else {
100 Err(ValidationErrors(self.violations))
101 }
102 }
103}
104
105pub trait Validate {
107 fn validate(&self) -> Result<(), ValidationErrors>;
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct ValidationErrors(Vec<Violation>);
112
113impl ValidationErrors {
114 pub fn violations(&self) -> &[Violation] {
115 &self.0
116 }
117
118 pub fn into_violations(self) -> Vec<Violation> {
119 self.0
120 }
121}
122
123impl fmt::Display for ValidationErrors {
124 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125 for (index, violation) in self.0.iter().enumerate() {
126 if index > 0 {
127 formatter.write_str("; ")?;
128 }
129 write!(
130 formatter,
131 "{}: {} ({})",
132 violation.field, violation.message, violation.code
133 )?;
134 }
135 Ok(())
136 }
137}
138
139impl std::error::Error for ValidationErrors {}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn collects_multiple_field_failures() {
147 let mut validation = Validation::new();
148 validation
149 .required("name", " ")
150 .range("age", 12, 18..=120)
151 .one_of("mode", &"broken", &["dev", "prod"]);
152
153 let errors = validation.finish().unwrap_err();
154 assert_eq!(errors.violations().len(), 3);
155 assert_eq!(errors.violations()[0].field, "name");
156 assert_eq!(errors.violations()[1].code, "range");
157 assert_eq!(errors.violations()[2].code, "one_of");
158 }
159
160 #[test]
161 fn measures_string_length_in_characters() {
162 let mut validation = Validation::new();
163 validation.length("name", "你好", 2..=2);
164 assert!(validation.finish().is_ok());
165 }
166}