reinhardt_forms/fields/
date_field.rs1use crate::field::{FieldError, FieldResult, FormField, Widget};
2use chrono::{Datelike, NaiveDate};
3
4pub struct DateField {
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 pub localize: bool,
14 pub locale: Option<String>,
15}
16
17impl DateField {
18 pub fn new(name: String) -> Self {
30 Self {
31 name,
32 label: None,
33 required: true,
34 help_text: None,
35 widget: Widget::DateInput,
36 initial: None,
37 input_formats: vec![
38 "%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(), ],
49 localize: false,
50 locale: None,
51 }
52 }
53 pub fn with_localize(mut self, localize: bool) -> Self {
64 self.localize = localize;
65 self
66 }
67 pub fn with_locale(mut self, locale: String) -> Self {
78 self.locale = Some(locale);
79 self
80 }
81
82 fn parse_date(&self, s: &str) -> Result<NaiveDate, String> {
83 for format in &self.input_formats {
84 if let Ok(date) = NaiveDate::parse_from_str(s, format) {
85 let year = date.year();
88 if !(1000..=9999).contains(&year) {
89 continue;
90 }
91 return Ok(date);
92 }
93 }
94 Err("Enter a valid date with a 4-digit year".to_string())
95 }
96}
97
98impl FormField for DateField {
99 fn name(&self) -> &str {
100 &self.name
101 }
102
103 fn label(&self) -> Option<&str> {
104 self.label.as_deref()
105 }
106
107 fn required(&self) -> bool {
108 self.required
109 }
110
111 fn help_text(&self) -> Option<&str> {
112 self.help_text.as_deref()
113 }
114
115 fn widget(&self) -> &Widget {
116 &self.widget
117 }
118
119 fn initial(&self) -> Option<&serde_json::Value> {
120 self.initial.as_ref()
121 }
122
123 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
124 match value {
125 None if self.required => Err(FieldError::required(None)),
126 None => Ok(serde_json::Value::Null),
127 Some(v) => {
128 let s = v
129 .as_str()
130 .ok_or_else(|| FieldError::Invalid("Expected string".to_string()))?;
131
132 let s = s.trim();
133
134 if s.is_empty() {
135 if self.required {
136 return Err(FieldError::required(None));
137 }
138 return Ok(serde_json::Value::Null);
139 }
140
141 let date = self.parse_date(s).map_err(FieldError::Validation)?;
142
143 Ok(serde_json::json!(date.format("%Y-%m-%d").to_string()))
145 }
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use rstest::rstest;
154
155 #[test]
156 fn test_date_field_required() {
157 let field = DateField::new("date".to_string());
158
159 assert!(field.clean(None).is_err());
161
162 assert!(field.clean(Some(&serde_json::json!(""))).is_err());
164 }
165
166 #[test]
167 fn test_date_field_not_required() {
168 let mut field = DateField::new("date".to_string());
169 field.required = false;
170
171 assert_eq!(field.clean(None).unwrap(), serde_json::Value::Null);
173
174 assert_eq!(
176 field.clean(Some(&serde_json::json!(""))).unwrap(),
177 serde_json::Value::Null
178 );
179 }
180
181 #[test]
182 fn test_date_field_iso_format() {
183 let field = DateField::new("date".to_string());
184
185 let result = field.clean(Some(&serde_json::json!("2025-01-15"))).unwrap();
187 assert_eq!(result, serde_json::json!("2025-01-15"));
188 }
189
190 #[test]
191 fn test_date_field_us_format() {
192 let field = DateField::new("date".to_string());
193
194 let result = field.clean(Some(&serde_json::json!("01/15/2025"))).unwrap();
196 assert_eq!(result, serde_json::json!("2025-01-15"));
197
198 assert!(field.clean(Some(&serde_json::json!("01/15/25"))).is_err());
200 }
201
202 #[test]
203 fn test_date_field_month_name_formats() {
204 let field = DateField::new("date".to_string());
205
206 let result = field
208 .clean(Some(&serde_json::json!("Jan 15 2025")))
209 .unwrap();
210 assert_eq!(result, serde_json::json!("2025-01-15"));
211
212 let result = field
214 .clean(Some(&serde_json::json!("Jan 15, 2025")))
215 .unwrap();
216 assert_eq!(result, serde_json::json!("2025-01-15"));
217
218 let result = field
220 .clean(Some(&serde_json::json!("January 15 2025")))
221 .unwrap();
222 assert_eq!(result, serde_json::json!("2025-01-15"));
223
224 let result = field
226 .clean(Some(&serde_json::json!("January 15, 2025")))
227 .unwrap();
228 assert_eq!(result, serde_json::json!("2025-01-15"));
229 }
230
231 #[test]
232 fn test_date_field_day_first_formats() {
233 let field = DateField::new("date".to_string());
234
235 let result = field
237 .clean(Some(&serde_json::json!("15 Jan 2025")))
238 .unwrap();
239 assert_eq!(result, serde_json::json!("2025-01-15"));
240
241 let result = field
243 .clean(Some(&serde_json::json!("15 January 2025")))
244 .unwrap();
245 assert_eq!(result, serde_json::json!("2025-01-15"));
246 }
247
248 #[test]
249 fn test_date_field_invalid_format() {
250 let field = DateField::new("date".to_string());
251
252 assert!(field.clean(Some(&serde_json::json!("not a date"))).is_err());
254 assert!(field.clean(Some(&serde_json::json!("2025/13/01"))).is_err());
255 assert!(field.clean(Some(&serde_json::json!("2025-00-01"))).is_err());
256 }
257
258 #[test]
259 fn test_date_field_whitespace_trimming() {
260 let field = DateField::new("date".to_string());
261
262 let result = field
264 .clean(Some(&serde_json::json!(" 2025-01-15 ")))
265 .unwrap();
266 assert_eq!(result, serde_json::json!("2025-01-15"));
267 }
268
269 #[test]
270 fn test_date_field_invalid_dates() {
271 let field = DateField::new("date".to_string());
272
273 assert!(field.clean(Some(&serde_json::json!("2025-13-01"))).is_err());
275
276 assert!(field.clean(Some(&serde_json::json!("2025-01-32"))).is_err());
278
279 assert!(field.clean(Some(&serde_json::json!("2025-02-30"))).is_err());
281 }
282
283 #[test]
284 fn test_date_field_leap_year() {
285 let field = DateField::new("date".to_string());
286
287 let result = field.clean(Some(&serde_json::json!("2024-02-29"))).unwrap();
289 assert_eq!(result, serde_json::json!("2024-02-29"));
290
291 assert!(field.clean(Some(&serde_json::json!("2025-02-29"))).is_err());
293 }
294
295 #[test]
296 fn test_date_field_localize() {
297 let field = DateField::new("date".to_string()).with_localize(true);
298 assert!(field.localize);
299 }
300
301 #[test]
302 fn test_date_field_locale() {
303 let field = DateField::new("date".to_string()).with_locale("en_US".to_string());
304 assert_eq!(field.locale, Some("en_US".to_string()));
305 }
306
307 #[test]
308 fn test_date_field_widget() {
309 let field = DateField::new("date".to_string());
310 assert!(matches!(field.widget(), &Widget::DateInput));
311 }
312
313 #[test]
314 fn test_date_field_name() {
315 let field = DateField::new("birth_date".to_string());
316 assert_eq!(field.name(), "birth_date");
317 }
318
319 #[rstest]
320 #[case("01/15/25")]
321 #[case("12/31/99")]
322 #[case("06/15/00")]
323 fn test_date_field_rejects_two_digit_years(#[case] input: &str) {
324 let field = DateField::new("date".to_string());
326
327 let result = field.clean(Some(&serde_json::json!(input)));
329
330 assert!(
332 result.is_err(),
333 "Expected 2-digit year input '{}' to be rejected, got: {:?}",
334 input,
335 result,
336 );
337 }
338
339 #[rstest]
340 #[case("01/15/2025", "2025-01-15")]
341 #[case("12/31/1999", "1999-12-31")]
342 #[case("2024-02-29", "2024-02-29")]
343 fn test_date_field_accepts_four_digit_years(#[case] input: &str, #[case] expected: &str) {
344 let field = DateField::new("date".to_string());
346
347 let result = field.clean(Some(&serde_json::json!(input)));
349
350 assert_eq!(
352 result.unwrap(),
353 serde_json::json!(expected),
354 "Expected 4-digit year input '{}' to parse as '{}'",
355 input,
356 expected,
357 );
358 }
359}