acton_htmx/forms/
error.rs1use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct FieldError {
11 pub message: String,
13 pub code: Option<String>,
15}
16
17impl FieldError {
18 #[must_use]
20 pub fn new(message: impl Into<String>) -> Self {
21 Self {
22 message: message.into(),
23 code: None,
24 }
25 }
26
27 #[must_use]
29 pub fn with_code(message: impl Into<String>, code: impl Into<String>) -> Self {
30 Self {
31 message: message.into(),
32 code: Some(code.into()),
33 }
34 }
35}
36
37impl std::fmt::Display for FieldError {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 write!(f, "{}", self.message)
40 }
41}
42
43#[derive(Debug, Clone, Default)]
59pub struct ValidationErrors {
60 errors: HashMap<String, Vec<FieldError>>,
61}
62
63impl ValidationErrors {
64 #[must_use]
66 pub fn new() -> Self {
67 Self::default()
68 }
69
70 pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
72 let field = field.into();
73 self.errors
74 .entry(field)
75 .or_default()
76 .push(FieldError::new(message));
77 }
78
79 pub fn add_with_code(
81 &mut self,
82 field: impl Into<String>,
83 message: impl Into<String>,
84 code: impl Into<String>,
85 ) {
86 let field = field.into();
87 self.errors
88 .entry(field)
89 .or_default()
90 .push(FieldError::with_code(message, code));
91 }
92
93 #[must_use]
95 pub fn has_errors(&self) -> bool {
96 !self.errors.is_empty()
97 }
98
99 #[must_use]
101 pub fn has_field_error(&self, field: &str) -> bool {
102 self.errors.contains_key(field)
103 }
104
105 #[must_use]
107 pub fn for_field(&self, field: &str) -> &[FieldError] {
108 self.errors.get(field).map_or(&[], Vec::as_slice)
109 }
110
111 #[must_use]
113 pub fn fields_with_errors(&self) -> Vec<&str> {
114 self.errors.keys().map(String::as_str).collect()
115 }
116
117 #[must_use]
119 pub fn count(&self) -> usize {
120 self.errors.values().map(Vec::len).sum()
121 }
122
123 pub fn clear(&mut self) {
125 self.errors.clear();
126 }
127
128 pub fn merge(&mut self, other: &Self) {
130 for (field, errors) in &other.errors {
131 self.errors
132 .entry(field.clone())
133 .or_default()
134 .extend(errors.iter().cloned());
135 }
136 }
137
138 pub fn iter(&self) -> impl Iterator<Item = (&str, &[FieldError])> {
140 self.errors
141 .iter()
142 .map(|(k, v)| (k.as_str(), v.as_slice()))
143 }
144}
145
146impl From<validator::ValidationErrors> for ValidationErrors {
150 fn from(errors: validator::ValidationErrors) -> Self {
151 let mut result = Self::new();
152 for (field, field_errors) in errors.field_errors() {
153 for error in field_errors {
154 let message = error
155 .message
156 .as_ref()
157 .map_or_else(|| error.code.to_string(), ToString::to_string);
158 result.add_with_code(field.to_string(), message, error.code.to_string());
159 }
160 }
161 result
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_field_error() {
171 let error = FieldError::new("is required");
172 assert_eq!(error.message, "is required");
173 assert!(error.code.is_none());
174 }
175
176 #[test]
177 fn test_field_error_with_code() {
178 let error = FieldError::with_code("is required", "required");
179 assert_eq!(error.message, "is required");
180 assert_eq!(error.code.as_deref(), Some("required"));
181 }
182
183 #[test]
184 fn test_validation_errors_new() {
185 let errors = ValidationErrors::new();
186 assert!(!errors.has_errors());
187 assert_eq!(errors.count(), 0);
188 }
189
190 #[test]
191 fn test_validation_errors_add() {
192 let mut errors = ValidationErrors::new();
193 errors.add("email", "is required");
194 errors.add("email", "is invalid");
195
196 assert!(errors.has_errors());
197 assert!(errors.has_field_error("email"));
198 assert!(!errors.has_field_error("password"));
199 assert_eq!(errors.for_field("email").len(), 2);
200 assert_eq!(errors.count(), 2);
201 }
202
203 #[test]
204 fn test_validation_errors_merge() {
205 let mut errors1 = ValidationErrors::new();
206 errors1.add("email", "is required");
207
208 let mut errors2 = ValidationErrors::new();
209 errors2.add("password", "too short");
210 errors2.add("email", "is invalid");
211
212 errors1.merge(&errors2);
213
214 assert_eq!(errors1.for_field("email").len(), 2);
215 assert_eq!(errors1.for_field("password").len(), 1);
216 assert_eq!(errors1.count(), 3);
217 }
218
219 #[test]
220 fn test_validation_errors_clear() {
221 let mut errors = ValidationErrors::new();
222 errors.add("email", "is required");
223 assert!(errors.has_errors());
224
225 errors.clear();
226 assert!(!errors.has_errors());
227 }
228
229 #[test]
230 fn test_fields_with_errors() {
231 let mut errors = ValidationErrors::new();
232 errors.add("email", "is required");
233 errors.add("password", "too short");
234
235 let fields = errors.fields_with_errors();
236 assert_eq!(fields.len(), 2);
237 assert!(fields.contains(&"email"));
238 assert!(fields.contains(&"password"));
239 }
240}