reinhardt_forms/fields/
datetime_field.rs1use crate::field::{FieldError, FieldResult, FormField, Widget};
2use chrono::NaiveDateTime;
3
4pub struct DateTimeField {
6 pub name: String,
7 pub label: Option<String>,
8 pub required: bool,
9 pub help_text: Option<String>,
10 pub widget: Widget,
11 pub initial: Option<serde_json::Value>,
12 pub input_formats: Vec<String>,
13}
14
15impl DateTimeField {
16 pub fn new(name: String) -> Self {
27 Self {
28 name,
29 label: None,
30 required: true,
31 help_text: None,
32 widget: Widget::TextInput,
33 initial: None,
34 input_formats: vec![
35 "%Y-%m-%d %H:%M:%S".to_string(),
36 "%Y-%m-%d %H:%M".to_string(),
37 "%Y-%m-%dT%H:%M:%S".to_string(),
38 "%Y-%m-%dT%H:%M".to_string(),
39 "%m/%d/%Y %H:%M:%S".to_string(),
40 "%m/%d/%Y %H:%M".to_string(),
41 "%m/%d/%y %H:%M:%S".to_string(),
42 "%m/%d/%y %H:%M".to_string(),
43 ],
44 }
45 }
46
47 fn parse_datetime(&self, s: &str) -> Result<NaiveDateTime, String> {
48 for fmt in &self.input_formats {
49 if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt) {
50 return Ok(dt);
51 }
52 }
53 Err("Enter a valid date/time".to_string())
54 }
55}
56
57impl FormField for DateTimeField {
58 fn name(&self) -> &str {
59 &self.name
60 }
61
62 fn label(&self) -> Option<&str> {
63 self.label.as_deref()
64 }
65
66 fn required(&self) -> bool {
67 self.required
68 }
69
70 fn help_text(&self) -> Option<&str> {
71 self.help_text.as_deref()
72 }
73
74 fn widget(&self) -> &Widget {
75 &self.widget
76 }
77
78 fn initial(&self) -> Option<&serde_json::Value> {
79 self.initial.as_ref()
80 }
81
82 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
83 match value {
84 None if self.required => Err(FieldError::required(None)),
85 None => Ok(serde_json::Value::Null),
86 Some(v) => {
87 let s = v
88 .as_str()
89 .ok_or_else(|| FieldError::Invalid("Expected string".to_string()))?;
90
91 let s = s.trim();
92
93 if s.is_empty() {
94 if self.required {
95 return Err(FieldError::required(None));
96 }
97 return Ok(serde_json::Value::Null);
98 }
99
100 let dt = self.parse_datetime(s).map_err(FieldError::Validation)?;
101
102 Ok(serde_json::Value::String(
103 dt.format("%Y-%m-%d %H:%M:%S").to_string(),
104 ))
105 }
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn test_datetimefield_valid() {
116 let field = DateTimeField::new("created_at".to_string());
117
118 assert_eq!(
119 field
120 .clean(Some(&serde_json::json!("2025-01-15 14:30:00")))
121 .unwrap(),
122 serde_json::json!("2025-01-15 14:30:00")
123 );
124 assert_eq!(
125 field
126 .clean(Some(&serde_json::json!("2025-01-15T14:30:00")))
127 .unwrap(),
128 serde_json::json!("2025-01-15 14:30:00")
129 );
130 assert_eq!(
131 field
132 .clean(Some(&serde_json::json!("01/15/2025 14:30:00")))
133 .unwrap(),
134 serde_json::json!("2025-01-15 14:30:00")
135 );
136 }
137
138 #[test]
139 fn test_datetimefield_invalid() {
140 let field = DateTimeField::new("created_at".to_string());
141
142 assert!(matches!(
143 field.clean(Some(&serde_json::json!("not a datetime"))),
144 Err(FieldError::Validation(_))
145 ));
146 assert!(matches!(
147 field.clean(Some(&serde_json::json!("2025-13-01 14:30:00"))),
148 Err(FieldError::Validation(_))
149 ));
150 }
151
152 #[test]
153 fn test_datetimefield_optional() {
154 let mut field = DateTimeField::new("created_at".to_string());
155 field.required = false;
156
157 assert_eq!(field.clean(None).unwrap(), serde_json::Value::Null);
158 assert_eq!(
159 field.clean(Some(&serde_json::json!(""))).unwrap(),
160 serde_json::Value::Null
161 );
162 }
163
164 #[test]
165 fn test_datetimefield_required() {
166 let field = DateTimeField::new("created_at".to_string());
167
168 assert!(field.clean(None).is_err());
170
171 assert!(field.clean(Some(&serde_json::json!(""))).is_err());
173 }
174
175 #[test]
176 fn test_datetimefield_iso_format_with_seconds() {
177 let field = DateTimeField::new("created_at".to_string());
178
179 let result = field
181 .clean(Some(&serde_json::json!("2025-01-15 14:30:00")))
182 .unwrap();
183 assert_eq!(result, serde_json::json!("2025-01-15 14:30:00"));
184
185 let result = field
187 .clean(Some(&serde_json::json!("2025-01-15T14:30:00")))
188 .unwrap();
189 assert_eq!(result, serde_json::json!("2025-01-15 14:30:00"));
190 }
191
192 #[test]
193 fn test_datetimefield_iso_format_without_seconds() {
194 let field = DateTimeField::new("created_at".to_string());
195
196 let result = field
198 .clean(Some(&serde_json::json!("2025-01-15 14:30")))
199 .unwrap();
200 assert_eq!(result, serde_json::json!("2025-01-15 14:30:00"));
201
202 let result = field
204 .clean(Some(&serde_json::json!("2025-01-15T14:30")))
205 .unwrap();
206 assert_eq!(result, serde_json::json!("2025-01-15 14:30:00"));
207 }
208
209 #[test]
210 fn test_datetimefield_us_format_with_seconds() {
211 let field = DateTimeField::new("created_at".to_string());
212
213 let result = field
215 .clean(Some(&serde_json::json!("01/15/2025 14:30:00")))
216 .unwrap();
217 assert_eq!(result, serde_json::json!("2025-01-15 14:30:00"));
218
219 let result = field
221 .clean(Some(&serde_json::json!("01/15/25 14:30:00")))
222 .unwrap();
223 assert_eq!(result, serde_json::json!("0025-01-15 14:30:00"));
224 }
225
226 #[test]
227 fn test_datetimefield_us_format_without_seconds() {
228 let field = DateTimeField::new("created_at".to_string());
229
230 let result = field
232 .clean(Some(&serde_json::json!("01/15/2025 14:30")))
233 .unwrap();
234 assert_eq!(result, serde_json::json!("2025-01-15 14:30:00"));
235
236 let result = field
238 .clean(Some(&serde_json::json!("01/15/25 14:30")))
239 .unwrap();
240 assert_eq!(result, serde_json::json!("0025-01-15 14:30:00"));
241 }
242
243 #[test]
244 fn test_datetimefield_whitespace_trimming() {
245 let field = DateTimeField::new("created_at".to_string());
246
247 let result = field
248 .clean(Some(&serde_json::json!(" 2025-01-15 14:30:00 ")))
249 .unwrap();
250 assert_eq!(result, serde_json::json!("2025-01-15 14:30:00"));
251 }
252
253 #[test]
254 fn test_datetimefield_invalid_date() {
255 let field = DateTimeField::new("created_at".to_string());
256
257 assert!(matches!(
259 field.clean(Some(&serde_json::json!("2025-13-01 14:30:00"))),
260 Err(FieldError::Validation(_))
261 ));
262
263 assert!(matches!(
265 field.clean(Some(&serde_json::json!("2025-01-32 14:30:00"))),
266 Err(FieldError::Validation(_))
267 ));
268
269 assert!(matches!(
271 field.clean(Some(&serde_json::json!("2025-02-30 14:30:00"))),
272 Err(FieldError::Validation(_))
273 ));
274 }
275
276 #[test]
277 fn test_datetimefield_invalid_time() {
278 let field = DateTimeField::new("created_at".to_string());
279
280 assert!(matches!(
282 field.clean(Some(&serde_json::json!("2025-01-15 25:30:00"))),
283 Err(FieldError::Validation(_))
284 ));
285
286 assert!(matches!(
288 field.clean(Some(&serde_json::json!("2025-01-15 14:61:00"))),
289 Err(FieldError::Validation(_))
290 ));
291
292 assert!(matches!(
294 field.clean(Some(&serde_json::json!("2025-01-15 14:30:61"))),
295 Err(FieldError::Validation(_))
296 ));
297 }
298
299 #[test]
300 fn test_datetimefield_leap_year() {
301 let field = DateTimeField::new("created_at".to_string());
302
303 let result = field
305 .clean(Some(&serde_json::json!("2024-02-29 14:30:00")))
306 .unwrap();
307 assert_eq!(result, serde_json::json!("2024-02-29 14:30:00"));
308
309 assert!(matches!(
311 field.clean(Some(&serde_json::json!("2025-02-29 14:30:00"))),
312 Err(FieldError::Validation(_))
313 ));
314 }
315
316 #[test]
317 fn test_datetimefield_midnight() {
318 let field = DateTimeField::new("created_at".to_string());
319
320 let result = field
321 .clean(Some(&serde_json::json!("2025-01-15 00:00:00")))
322 .unwrap();
323 assert_eq!(result, serde_json::json!("2025-01-15 00:00:00"));
324 }
325
326 #[test]
327 fn test_datetimefield_end_of_day() {
328 let field = DateTimeField::new("created_at".to_string());
329
330 let result = field
331 .clean(Some(&serde_json::json!("2025-01-15 23:59:59")))
332 .unwrap();
333 assert_eq!(result, serde_json::json!("2025-01-15 23:59:59"));
334 }
335
336 #[test]
337 fn test_datetimefield_noon() {
338 let field = DateTimeField::new("created_at".to_string());
339
340 let result = field
341 .clean(Some(&serde_json::json!("2025-01-15 12:00:00")))
342 .unwrap();
343 assert_eq!(result, serde_json::json!("2025-01-15 12:00:00"));
344 }
345
346 #[test]
347 fn test_datetimefield_widget_type() {
348 let field = DateTimeField::new("created_at".to_string());
349 assert!(matches!(field.widget(), &Widget::TextInput));
350 }
351
352 #[test]
353 fn test_datetimefield_custom_formats() {
354 let mut field = DateTimeField::new("created_at".to_string());
355 field.input_formats.push("%d-%m-%Y %H:%M:%S".to_string());
357
358 let result = field
360 .clean(Some(&serde_json::json!("15-01-2025 14:30:00")))
361 .unwrap();
362 assert_eq!(result, serde_json::json!("2025-01-15 14:30:00"));
363 }
364
365 #[test]
366 fn test_datetimefield_format_precedence() {
367 let field = DateTimeField::new("created_at".to_string());
368
369 let result = field
372 .clean(Some(&serde_json::json!("2025-01-15 14:30:00")))
373 .unwrap();
374 assert_eq!(result, serde_json::json!("2025-01-15 14:30:00"));
375 }
376}