Skip to main content

reinhardt_forms/fields/
datetime_field.rs

1use crate::field::{FieldError, FieldResult, FormField, Widget};
2use chrono::NaiveDateTime;
3
4/// DateTimeField for date and time input
5pub 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	/// Create a new DateTimeField
17	///
18	/// # Examples
19	///
20	/// ```
21	/// use reinhardt_forms::fields::DateTimeField;
22	///
23	/// let field = DateTimeField::new("event_time".to_string());
24	/// assert_eq!(field.name, "event_time");
25	/// ```
26	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		// Required field rejects None
169		assert!(field.clean(None).is_err());
170
171		// Required field rejects empty string
172		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		// ISO 8601 with space separator
180		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		// ISO 8601 with T separator
186		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		// ISO 8601 with space separator (no seconds)
197		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		// ISO 8601 with T separator (no seconds)
203		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		// US format with 4-digit year
214		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		// US format with 2-digit year (chrono interprets as 00-99 AD)
220		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		// US format with 4-digit year (no seconds)
231		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		// US format with 2-digit year (no seconds)
237		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		// Invalid month (13)
258		assert!(matches!(
259			field.clean(Some(&serde_json::json!("2025-13-01 14:30:00"))),
260			Err(FieldError::Validation(_))
261		));
262
263		// Invalid day (32)
264		assert!(matches!(
265			field.clean(Some(&serde_json::json!("2025-01-32 14:30:00"))),
266			Err(FieldError::Validation(_))
267		));
268
269		// Feb 30 (invalid)
270		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		// Invalid hour (25)
281		assert!(matches!(
282			field.clean(Some(&serde_json::json!("2025-01-15 25:30:00"))),
283			Err(FieldError::Validation(_))
284		));
285
286		// Invalid minute (61)
287		assert!(matches!(
288			field.clean(Some(&serde_json::json!("2025-01-15 14:61:00"))),
289			Err(FieldError::Validation(_))
290		));
291
292		// Invalid second (61)
293		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		// Feb 29 in leap year 2024 should be valid
304		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		// Feb 29 in non-leap year 2025 should fail
310		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		// Add custom format
356		field.input_formats.push("%d-%m-%Y %H:%M:%S".to_string());
357
358		// Custom day-first format should work
359		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		// When multiple formats could match, first matching format is used
370		// "2025-01-15 14:30:00" matches "%Y-%m-%d %H:%M:%S" (first format)
371		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}