reinhardt_forms/fields/
date_field.rs1use crate::field::{FieldError, FieldResult, FormField, Widget};
2use chrono::{Datelike, NaiveDate};
3
4#[derive(Debug, Clone)]
6pub struct DateField {
7 pub name: String,
9 pub label: Option<String>,
11 pub required: bool,
13 pub help_text: Option<String>,
15 pub widget: Widget,
17 pub initial: Option<serde_json::Value>,
19 pub input_formats: Vec<String>,
21 pub localize: bool,
23 pub locale: Option<String>,
25}
26
27impl DateField {
28 pub fn new(name: String) -> Self {
40 Self {
41 name,
42 label: None,
43 required: true,
44 help_text: None,
45 widget: Widget::DateInput,
46 initial: None,
47 input_formats: vec![
48 "%Y-%m-%d".to_string(), "%m/%d/%Y".to_string(), "%b %d %Y".to_string(), "%b %d, %Y".to_string(), "%d %b %Y".to_string(), "%d %b, %Y".to_string(), "%B %d %Y".to_string(), "%B %d, %Y".to_string(), "%d %B %Y".to_string(), "%d %B, %Y".to_string(), ],
59 localize: false,
60 locale: None,
61 }
62 }
63 pub fn with_localize(mut self, localize: bool) -> Self {
74 self.localize = localize;
75 self
76 }
77 pub fn with_locale(mut self, locale: String) -> Self {
88 self.locale = Some(locale);
89 self
90 }
91
92 fn parse_date(&self, s: &str) -> Result<NaiveDate, String> {
93 for format in &self.input_formats {
94 if let Ok(date) = NaiveDate::parse_from_str(s, format) {
95 let year = date.year();
98 if !(1000..=9999).contains(&year) {
99 continue;
100 }
101 return Ok(date);
102 }
103 }
104 Err("Enter a valid date with a 4-digit year".to_string())
105 }
106}
107
108impl FormField for DateField {
109 fn name(&self) -> &str {
110 &self.name
111 }
112
113 fn label(&self) -> Option<&str> {
114 self.label.as_deref()
115 }
116
117 fn required(&self) -> bool {
118 self.required
119 }
120
121 fn help_text(&self) -> Option<&str> {
122 self.help_text.as_deref()
123 }
124
125 fn widget(&self) -> &Widget {
126 &self.widget
127 }
128
129 fn initial(&self) -> Option<&serde_json::Value> {
130 self.initial.as_ref()
131 }
132
133 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
134 match value {
135 None if self.required => Err(FieldError::required(None)),
136 None => Ok(serde_json::Value::Null),
137 Some(v) => {
138 let s = v
139 .as_str()
140 .ok_or_else(|| FieldError::Invalid("Expected string".to_string()))?;
141
142 let s = s.trim();
143
144 if s.is_empty() {
145 if self.required {
146 return Err(FieldError::required(None));
147 }
148 return Ok(serde_json::Value::Null);
149 }
150
151 let date = self.parse_date(s).map_err(FieldError::Validation)?;
152
153 Ok(serde_json::json!(date.format("%Y-%m-%d").to_string()))
155 }
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use rstest::rstest;
164
165 #[test]
166 fn test_date_field_required() {
167 let field = DateField::new("date".to_string());
168
169 assert!(field.clean(None).is_err());
171
172 assert!(field.clean(Some(&serde_json::json!(""))).is_err());
174 }
175
176 #[test]
177 fn test_date_field_not_required() {
178 let mut field = DateField::new("date".to_string());
179 field.required = false;
180
181 assert_eq!(field.clean(None).unwrap(), serde_json::Value::Null);
183
184 assert_eq!(
186 field.clean(Some(&serde_json::json!(""))).unwrap(),
187 serde_json::Value::Null
188 );
189 }
190
191 #[test]
192 fn test_date_field_iso_format() {
193 let field = DateField::new("date".to_string());
194
195 let result = field.clean(Some(&serde_json::json!("2025-01-15"))).unwrap();
197 assert_eq!(result, serde_json::json!("2025-01-15"));
198 }
199
200 #[test]
201 fn test_date_field_us_format() {
202 let field = DateField::new("date".to_string());
203
204 let result = field.clean(Some(&serde_json::json!("01/15/2025"))).unwrap();
206 assert_eq!(result, serde_json::json!("2025-01-15"));
207
208 assert!(field.clean(Some(&serde_json::json!("01/15/25"))).is_err());
210 }
211
212 #[test]
213 fn test_date_field_month_name_formats() {
214 let field = DateField::new("date".to_string());
215
216 let result = field
218 .clean(Some(&serde_json::json!("Jan 15 2025")))
219 .unwrap();
220 assert_eq!(result, serde_json::json!("2025-01-15"));
221
222 let result = field
224 .clean(Some(&serde_json::json!("Jan 15, 2025")))
225 .unwrap();
226 assert_eq!(result, serde_json::json!("2025-01-15"));
227
228 let result = field
230 .clean(Some(&serde_json::json!("January 15 2025")))
231 .unwrap();
232 assert_eq!(result, serde_json::json!("2025-01-15"));
233
234 let result = field
236 .clean(Some(&serde_json::json!("January 15, 2025")))
237 .unwrap();
238 assert_eq!(result, serde_json::json!("2025-01-15"));
239 }
240
241 #[test]
242 fn test_date_field_day_first_formats() {
243 let field = DateField::new("date".to_string());
244
245 let result = field
247 .clean(Some(&serde_json::json!("15 Jan 2025")))
248 .unwrap();
249 assert_eq!(result, serde_json::json!("2025-01-15"));
250
251 let result = field
253 .clean(Some(&serde_json::json!("15 January 2025")))
254 .unwrap();
255 assert_eq!(result, serde_json::json!("2025-01-15"));
256 }
257
258 #[test]
259 fn test_date_field_invalid_format() {
260 let field = DateField::new("date".to_string());
261
262 assert!(field.clean(Some(&serde_json::json!("not a date"))).is_err());
264 assert!(field.clean(Some(&serde_json::json!("2025/13/01"))).is_err());
265 assert!(field.clean(Some(&serde_json::json!("2025-00-01"))).is_err());
266 }
267
268 #[test]
269 fn test_date_field_whitespace_trimming() {
270 let field = DateField::new("date".to_string());
271
272 let result = field
274 .clean(Some(&serde_json::json!(" 2025-01-15 ")))
275 .unwrap();
276 assert_eq!(result, serde_json::json!("2025-01-15"));
277 }
278
279 #[test]
280 fn test_date_field_invalid_dates() {
281 let field = DateField::new("date".to_string());
282
283 assert!(field.clean(Some(&serde_json::json!("2025-13-01"))).is_err());
285
286 assert!(field.clean(Some(&serde_json::json!("2025-01-32"))).is_err());
288
289 assert!(field.clean(Some(&serde_json::json!("2025-02-30"))).is_err());
291 }
292
293 #[test]
294 fn test_date_field_leap_year() {
295 let field = DateField::new("date".to_string());
296
297 let result = field.clean(Some(&serde_json::json!("2024-02-29"))).unwrap();
299 assert_eq!(result, serde_json::json!("2024-02-29"));
300
301 assert!(field.clean(Some(&serde_json::json!("2025-02-29"))).is_err());
303 }
304
305 #[test]
306 fn test_date_field_localize() {
307 let field = DateField::new("date".to_string()).with_localize(true);
308 assert!(field.localize);
309 }
310
311 #[test]
312 fn test_date_field_locale() {
313 let field = DateField::new("date".to_string()).with_locale("en_US".to_string());
314 assert_eq!(field.locale, Some("en_US".to_string()));
315 }
316
317 #[test]
318 fn test_date_field_widget() {
319 let field = DateField::new("date".to_string());
320 assert!(matches!(field.widget(), &Widget::DateInput));
321 }
322
323 #[test]
324 fn test_date_field_name() {
325 let field = DateField::new("birth_date".to_string());
326 assert_eq!(field.name(), "birth_date");
327 }
328
329 #[rstest]
330 #[case("01/15/25")]
331 #[case("12/31/99")]
332 #[case("06/15/00")]
333 fn test_date_field_rejects_two_digit_years(#[case] input: &str) {
334 let field = DateField::new("date".to_string());
336
337 let result = field.clean(Some(&serde_json::json!(input)));
339
340 assert!(
342 result.is_err(),
343 "Expected 2-digit year input '{}' to be rejected, got: {:?}",
344 input,
345 result,
346 );
347 }
348
349 #[rstest]
350 #[case("01/15/2025", "2025-01-15")]
351 #[case("12/31/1999", "1999-12-31")]
352 #[case("2024-02-29", "2024-02-29")]
353 fn test_date_field_accepts_four_digit_years(#[case] input: &str, #[case] expected: &str) {
354 let field = DateField::new("date".to_string());
356
357 let result = field.clean(Some(&serde_json::json!(input)));
359
360 assert_eq!(
362 result.unwrap(),
363 serde_json::json!(expected),
364 "Expected 4-digit year input '{}' to parse as '{}'",
365 input,
366 expected,
367 );
368 }
369}