Skip to main content

reinhardt_forms/fields/
time_field.rs

1use crate::field::{FieldError, FieldResult, FormField, Widget};
2use chrono::NaiveTime;
3
4/// TimeField for time input
5pub struct TimeField {
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 TimeField {
16	/// Create a new TimeField
17	///
18	/// # Examples
19	///
20	/// ```
21	/// use reinhardt_forms::fields::TimeField;
22	///
23	/// let field = TimeField::new("start_time".to_string());
24	/// assert_eq!(field.name, "start_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				"%H:%M:%S".to_string(),
36				"%H:%M".to_string(),
37				"%I:%M:%S %p".to_string(),
38				"%I:%M %p".to_string(),
39			],
40		}
41	}
42
43	fn parse_time(&self, s: &str) -> Result<NaiveTime, String> {
44		for fmt in &self.input_formats {
45			if let Ok(time) = NaiveTime::parse_from_str(s, fmt) {
46				return Ok(time);
47			}
48		}
49		Err("Enter a valid time".to_string())
50	}
51}
52
53impl FormField for TimeField {
54	fn name(&self) -> &str {
55		&self.name
56	}
57
58	fn label(&self) -> Option<&str> {
59		self.label.as_deref()
60	}
61
62	fn required(&self) -> bool {
63		self.required
64	}
65
66	fn help_text(&self) -> Option<&str> {
67		self.help_text.as_deref()
68	}
69
70	fn widget(&self) -> &Widget {
71		&self.widget
72	}
73
74	fn initial(&self) -> Option<&serde_json::Value> {
75		self.initial.as_ref()
76	}
77
78	fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
79		match value {
80			None if self.required => Err(FieldError::required(None)),
81			None => Ok(serde_json::Value::Null),
82			Some(v) => {
83				let s = v
84					.as_str()
85					.ok_or_else(|| FieldError::Invalid("Expected string".to_string()))?;
86
87				let s = s.trim();
88
89				if s.is_empty() {
90					if self.required {
91						return Err(FieldError::required(None));
92					}
93					return Ok(serde_json::Value::Null);
94				}
95
96				let time = self.parse_time(s).map_err(FieldError::Validation)?;
97
98				Ok(serde_json::Value::String(
99					time.format("%H:%M:%S").to_string(),
100				))
101			}
102		}
103	}
104}
105
106#[cfg(test)]
107mod tests {
108	use super::*;
109
110	#[test]
111	fn test_timefield_valid() {
112		let field = TimeField::new("start_time".to_string());
113
114		assert_eq!(
115			field.clean(Some(&serde_json::json!("14:30:00"))).unwrap(),
116			serde_json::json!("14:30:00")
117		);
118		assert_eq!(
119			field.clean(Some(&serde_json::json!("14:30"))).unwrap(),
120			serde_json::json!("14:30:00")
121		);
122		assert_eq!(
123			field
124				.clean(Some(&serde_json::json!("02:30:00 PM")))
125				.unwrap(),
126			serde_json::json!("14:30:00")
127		);
128	}
129
130	#[test]
131	fn test_timefield_invalid() {
132		let field = TimeField::new("start_time".to_string());
133
134		assert!(matches!(
135			field.clean(Some(&serde_json::json!("not a time"))),
136			Err(FieldError::Validation(_))
137		));
138		assert!(matches!(
139			field.clean(Some(&serde_json::json!("25:00:00"))),
140			Err(FieldError::Validation(_))
141		));
142	}
143
144	#[test]
145	fn test_timefield_optional() {
146		let mut field = TimeField::new("start_time".to_string());
147		field.required = false;
148
149		assert_eq!(field.clean(None).unwrap(), serde_json::Value::Null);
150		assert_eq!(
151			field.clean(Some(&serde_json::json!(""))).unwrap(),
152			serde_json::Value::Null
153		);
154	}
155
156	#[test]
157	fn test_timefield_required() {
158		let field = TimeField::new("start_time".to_string());
159
160		// Required field rejects None
161		assert!(field.clean(None).is_err());
162
163		// Required field rejects empty string
164		assert!(field.clean(Some(&serde_json::json!(""))).is_err());
165	}
166
167	#[test]
168	fn test_timefield_24hour_format_with_seconds() {
169		let field = TimeField::new("start_time".to_string());
170
171		let result = field.clean(Some(&serde_json::json!("14:30:00"))).unwrap();
172		assert_eq!(result, serde_json::json!("14:30:00"));
173
174		let result = field.clean(Some(&serde_json::json!("09:15:30"))).unwrap();
175		assert_eq!(result, serde_json::json!("09:15:30"));
176	}
177
178	#[test]
179	fn test_timefield_24hour_format_without_seconds() {
180		let field = TimeField::new("start_time".to_string());
181
182		// Without seconds defaults to :00
183		let result = field.clean(Some(&serde_json::json!("14:30"))).unwrap();
184		assert_eq!(result, serde_json::json!("14:30:00"));
185
186		let result = field.clean(Some(&serde_json::json!("09:15"))).unwrap();
187		assert_eq!(result, serde_json::json!("09:15:00"));
188	}
189
190	#[test]
191	fn test_timefield_12hour_format_pm() {
192		let field = TimeField::new("start_time".to_string());
193
194		// PM times
195		let result = field
196			.clean(Some(&serde_json::json!("02:30:00 PM")))
197			.unwrap();
198		assert_eq!(result, serde_json::json!("14:30:00"));
199
200		let result = field.clean(Some(&serde_json::json!("02:30 PM"))).unwrap();
201		assert_eq!(result, serde_json::json!("14:30:00"));
202
203		let result = field
204			.clean(Some(&serde_json::json!("11:59:59 PM")))
205			.unwrap();
206		assert_eq!(result, serde_json::json!("23:59:59"));
207	}
208
209	#[test]
210	fn test_timefield_12hour_format_am() {
211		let field = TimeField::new("start_time".to_string());
212
213		// AM times
214		let result = field
215			.clean(Some(&serde_json::json!("09:30:00 AM")))
216			.unwrap();
217		assert_eq!(result, serde_json::json!("09:30:00"));
218
219		let result = field.clean(Some(&serde_json::json!("09:30 AM"))).unwrap();
220		assert_eq!(result, serde_json::json!("09:30:00"));
221
222		let result = field
223			.clean(Some(&serde_json::json!("11:59:59 AM")))
224			.unwrap();
225		assert_eq!(result, serde_json::json!("11:59:59"));
226	}
227
228	#[test]
229	fn test_timefield_midnight() {
230		let field = TimeField::new("start_time".to_string());
231
232		// Midnight as 00:00:00
233		let result = field.clean(Some(&serde_json::json!("00:00:00"))).unwrap();
234		assert_eq!(result, serde_json::json!("00:00:00"));
235
236		// Midnight as 12:00 AM
237		let result = field.clean(Some(&serde_json::json!("12:00 AM"))).unwrap();
238		assert_eq!(result, serde_json::json!("00:00:00"));
239	}
240
241	#[test]
242	fn test_timefield_noon() {
243		let field = TimeField::new("start_time".to_string());
244
245		// Noon as 12:00:00
246		let result = field.clean(Some(&serde_json::json!("12:00:00"))).unwrap();
247		assert_eq!(result, serde_json::json!("12:00:00"));
248
249		// Noon as 12:00 PM
250		let result = field.clean(Some(&serde_json::json!("12:00 PM"))).unwrap();
251		assert_eq!(result, serde_json::json!("12:00:00"));
252	}
253
254	#[test]
255	fn test_timefield_end_of_day() {
256		let field = TimeField::new("start_time".to_string());
257
258		let result = field.clean(Some(&serde_json::json!("23:59:59"))).unwrap();
259		assert_eq!(result, serde_json::json!("23:59:59"));
260	}
261
262	#[test]
263	fn test_timefield_whitespace_trimming() {
264		let field = TimeField::new("start_time".to_string());
265
266		let result = field
267			.clean(Some(&serde_json::json!("  14:30:00  ")))
268			.unwrap();
269		assert_eq!(result, serde_json::json!("14:30:00"));
270	}
271
272	#[test]
273	fn test_timefield_invalid_hour() {
274		let field = TimeField::new("start_time".to_string());
275
276		// Hour 25 is invalid
277		assert!(matches!(
278			field.clean(Some(&serde_json::json!("25:00:00"))),
279			Err(FieldError::Validation(_))
280		));
281
282		// Hour 24 is invalid (valid range is 00-23)
283		assert!(matches!(
284			field.clean(Some(&serde_json::json!("24:00:00"))),
285			Err(FieldError::Validation(_))
286		));
287	}
288
289	#[test]
290	fn test_timefield_invalid_minute() {
291		let field = TimeField::new("start_time".to_string());
292
293		// Minute 60 is invalid
294		assert!(matches!(
295			field.clean(Some(&serde_json::json!("14:60:00"))),
296			Err(FieldError::Validation(_))
297		));
298
299		// Minute 99 is invalid
300		assert!(matches!(
301			field.clean(Some(&serde_json::json!("14:99:00"))),
302			Err(FieldError::Validation(_))
303		));
304	}
305
306	#[test]
307	fn test_timefield_invalid_second() {
308		let field = TimeField::new("start_time".to_string());
309
310		// Second 99 is invalid
311		assert!(matches!(
312			field.clean(Some(&serde_json::json!("14:30:99"))),
313			Err(FieldError::Validation(_))
314		));
315
316		// Note: Second 60 is accepted by chrono as leap second
317		// So we test with a clearly invalid value like 99 instead
318	}
319
320	#[test]
321	fn test_timefield_invalid_format() {
322		let field = TimeField::new("start_time".to_string());
323
324		// Missing colon
325		assert!(matches!(
326			field.clean(Some(&serde_json::json!("1430"))),
327			Err(FieldError::Validation(_))
328		));
329
330		// Invalid text
331		assert!(matches!(
332			field.clean(Some(&serde_json::json!("not a time"))),
333			Err(FieldError::Validation(_))
334		));
335	}
336
337	#[test]
338	fn test_timefield_widget_type() {
339		let field = TimeField::new("start_time".to_string());
340		assert!(matches!(field.widget(), &Widget::TextInput));
341	}
342
343	#[test]
344	fn test_timefield_custom_format() {
345		let mut field = TimeField::new("start_time".to_string());
346		// Replace with custom 24-hour format using period
347		field.input_formats.clear();
348		field.input_formats.push("%H.%M.%S".to_string());
349
350		// Custom format with periods should work
351		let result = field.clean(Some(&serde_json::json!("14.30.00"))).unwrap();
352		assert_eq!(result, serde_json::json!("14:30:00"));
353	}
354
355	#[test]
356	fn test_timefield_format_precedence() {
357		let field = TimeField::new("start_time".to_string());
358
359		// When multiple formats could match, first matching format is used
360		// "14:30:00" matches "%H:%M:%S" (first format)
361		let result = field.clean(Some(&serde_json::json!("14:30:00"))).unwrap();
362		assert_eq!(result, serde_json::json!("14:30:00"));
363	}
364}