Skip to main content

reinhardt_forms/fields/
multi_value_field.rs

1use crate::field::{FieldError, FieldResult, FormField, Widget};
2use chrono::NaiveDateTime;
3
4/// MultiValueField combines multiple fields into one
5pub struct MultiValueField {
6	/// The field name used as the form data key.
7	pub name: String,
8	/// Optional human-readable label for display.
9	pub label: Option<String>,
10	/// Whether this field must be filled in.
11	pub required: bool,
12	/// Optional help text displayed alongside the field.
13	pub help_text: Option<String>,
14	/// The widget type used for rendering this field.
15	pub widget: Widget,
16	/// Optional initial (default) value for the field.
17	pub initial: Option<serde_json::Value>,
18	/// The sub-fields that make up this composite field.
19	pub fields: Vec<Box<dyn FormField>>,
20	/// Whether all sub-fields must have values.
21	pub require_all_fields: bool,
22}
23
24impl MultiValueField {
25	/// Create a new MultiValueField
26	///
27	/// # Examples
28	///
29	/// ```
30	/// use reinhardt_forms::fields::MultiValueField;
31	/// use reinhardt_forms::fields::{CharField, IntegerField};
32	/// use reinhardt_forms::FormField;
33	/// use serde_json::json;
34	///
35	/// // Create a multi-value field combining name and age
36	/// let fields: Vec<Box<dyn FormField>> = vec![
37	///     Box::new(CharField::new("name".to_string())),
38	///     Box::new(IntegerField::new("age".to_string())),
39	/// ];
40	///
41	/// let field = MultiValueField::new("person".to_string(), fields);
42	///
43	/// // Valid: both values provided
44	/// let result = field.clean(Some(&json!(["John Doe", 30])));
45	/// assert!(result.is_ok());
46	/// ```
47	pub fn new(name: String, fields: Vec<Box<dyn FormField>>) -> Self {
48		Self {
49			name,
50			label: None,
51			required: true,
52			help_text: None,
53			widget: Widget::TextInput,
54			initial: None,
55			fields,
56			require_all_fields: true,
57		}
58	}
59	/// Compresses the cleaned sub-field values into a single value.
60	pub fn compress(&self, values: Vec<serde_json::Value>) -> FieldResult<serde_json::Value> {
61		// Default implementation: return array of values
62		Ok(serde_json::Value::Array(values))
63	}
64}
65
66impl FormField for MultiValueField {
67	fn name(&self) -> &str {
68		&self.name
69	}
70
71	fn label(&self) -> Option<&str> {
72		self.label.as_deref()
73	}
74
75	fn required(&self) -> bool {
76		self.required
77	}
78
79	fn help_text(&self) -> Option<&str> {
80		self.help_text.as_deref()
81	}
82
83	fn widget(&self) -> &Widget {
84		&self.widget
85	}
86
87	fn initial(&self) -> Option<&serde_json::Value> {
88		self.initial.as_ref()
89	}
90
91	fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
92		match value {
93			None if self.required => Err(FieldError::required(None)),
94			None => Ok(serde_json::Value::Null),
95			Some(v) => {
96				let values = v
97					.as_array()
98					.ok_or_else(|| FieldError::invalid(None, "Expected array"))?;
99
100				if values.is_empty() && self.required {
101					return Err(FieldError::required(None));
102				}
103
104				if values.len() != self.fields.len() {
105					return Err(FieldError::invalid(
106						None,
107						&format!("Expected {} values", self.fields.len()),
108					));
109				}
110
111				let mut cleaned_values = Vec::new();
112				for (idx, field) in self.fields.iter().enumerate() {
113					let field_value = values.get(idx);
114
115					match field.clean(field_value) {
116						Ok(cleaned) => {
117							if cleaned.is_null() && self.require_all_fields {
118								return Err(FieldError::validation(
119									None,
120									"All fields are required",
121								));
122							}
123							cleaned_values.push(cleaned);
124						}
125						Err(e) => return Err(e),
126					}
127				}
128
129				self.compress(cleaned_values)
130			}
131		}
132	}
133}
134
135/// SplitDateTimeField splits datetime input into separate date and time fields
136pub struct SplitDateTimeField {
137	/// The field name used as the form data key.
138	pub name: String,
139	/// Optional human-readable label for display.
140	pub label: Option<String>,
141	/// Whether this field must be filled in.
142	pub required: bool,
143	/// Optional help text displayed alongside the field.
144	pub help_text: Option<String>,
145	/// The widget type used for rendering this field.
146	pub widget: Widget,
147	/// Optional initial (default) value for the field.
148	pub initial: Option<serde_json::Value>,
149	/// Accepted date format strings for the date portion.
150	pub input_date_formats: Vec<String>,
151	/// Accepted time format strings for the time portion.
152	pub input_time_formats: Vec<String>,
153}
154
155impl SplitDateTimeField {
156	/// Creates a new `SplitDateTimeField` with default date and time formats.
157	pub fn new(name: String) -> Self {
158		Self {
159			name,
160			label: None,
161			required: true,
162			help_text: None,
163			widget: Widget::TextInput,
164			initial: None,
165			input_date_formats: vec![
166				"%Y-%m-%d".to_string(),
167				"%m/%d/%Y".to_string(),
168				"%m/%d/%y".to_string(),
169			],
170			input_time_formats: vec![
171				"%H:%M:%S".to_string(),
172				"%H:%M".to_string(),
173				"%I:%M:%S %p".to_string(),
174				"%I:%M %p".to_string(),
175			],
176		}
177	}
178
179	fn parse_date(&self, s: &str) -> Result<chrono::NaiveDate, String> {
180		for fmt in &self.input_date_formats {
181			if let Ok(date) = chrono::NaiveDate::parse_from_str(s, fmt) {
182				return Ok(date);
183			}
184		}
185		Err("Enter a valid date".to_string())
186	}
187
188	fn parse_time(&self, s: &str) -> Result<chrono::NaiveTime, String> {
189		for fmt in &self.input_time_formats {
190			if let Ok(time) = chrono::NaiveTime::parse_from_str(s, fmt) {
191				return Ok(time);
192			}
193		}
194		Err("Enter a valid time".to_string())
195	}
196}
197
198impl FormField for SplitDateTimeField {
199	fn name(&self) -> &str {
200		&self.name
201	}
202
203	fn label(&self) -> Option<&str> {
204		self.label.as_deref()
205	}
206
207	fn required(&self) -> bool {
208		self.required
209	}
210
211	fn help_text(&self) -> Option<&str> {
212		self.help_text.as_deref()
213	}
214
215	fn widget(&self) -> &Widget {
216		&self.widget
217	}
218
219	fn initial(&self) -> Option<&serde_json::Value> {
220		self.initial.as_ref()
221	}
222
223	fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
224		match value {
225			None if self.required => Err(FieldError::required(None)),
226			None => Ok(serde_json::Value::Null),
227			Some(v) => {
228				// Expect an array with [date_string, time_string]
229				let parts = v
230					.as_array()
231					.ok_or_else(|| FieldError::invalid(None, "Expected array of [date, time]"))?;
232
233				if parts.len() != 2 {
234					return Err(FieldError::invalid(None, "Expected [date, time]"));
235				}
236
237				let date_str = parts[0]
238					.as_str()
239					.ok_or_else(|| FieldError::invalid(None, "Date must be a string"))?;
240
241				let time_str = parts[1]
242					.as_str()
243					.ok_or_else(|| FieldError::invalid(None, "Time must be a string"))?;
244
245				if date_str.trim().is_empty() || time_str.trim().is_empty() {
246					if self.required {
247						return Err(FieldError::required(None));
248					}
249					return Ok(serde_json::Value::Null);
250				}
251
252				let date = self
253					.parse_date(date_str.trim())
254					.map_err(|e| FieldError::validation(None, &e))?;
255
256				let time = self
257					.parse_time(time_str.trim())
258					.map_err(|e| FieldError::validation(None, &e))?;
259
260				let datetime = NaiveDateTime::new(date, time);
261
262				Ok(serde_json::Value::String(
263					datetime.format("%Y-%m-%d %H:%M:%S").to_string(),
264				))
265			}
266		}
267	}
268}
269
270#[cfg(test)]
271mod tests {
272	use super::*;
273	use crate::fields::{CharField, IntegerField};
274
275	#[test]
276	fn test_multi_value_field() {
277		let fields: Vec<Box<dyn FormField>> = vec![
278			Box::new(CharField::new("first".to_string())),
279			Box::new(IntegerField::new("second".to_string())),
280		];
281
282		let field = MultiValueField::new("combined".to_string(), fields);
283
284		let value = serde_json::json!(["hello", 42]);
285		let result = field.clean(Some(&value));
286		assert!(result.is_ok());
287
288		// Test with wrong number of values
289		let wrong_value = serde_json::json!(["hello"]);
290		assert!(matches!(
291			field.clean(Some(&wrong_value)),
292			Err(FieldError::Invalid(_))
293		));
294	}
295
296	#[test]
297	fn test_split_datetime_field() {
298		let field = SplitDateTimeField::new("when".to_string());
299
300		let value = serde_json::json!(["2025-01-15", "14:30:00"]);
301		let result = field.clean(Some(&value)).unwrap();
302		assert_eq!(result, serde_json::json!("2025-01-15 14:30:00"));
303
304		// Test with different formats
305		let value2 = serde_json::json!(["01/15/2025", "02:30 PM"]);
306		let result2 = field.clean(Some(&value2)).unwrap();
307		assert_eq!(result2, serde_json::json!("2025-01-15 14:30:00"));
308	}
309
310	#[test]
311	fn test_split_datetime_field_invalid() {
312		let field = SplitDateTimeField::new("when".to_string());
313
314		// Wrong format
315		let value = serde_json::json!(["not-a-date", "14:30:00"]);
316		assert!(matches!(
317			field.clean(Some(&value)),
318			Err(FieldError::Validation(_))
319		));
320
321		// Wrong structure
322		let value2 = serde_json::json!(["2025-01-15"]);
323		assert!(matches!(
324			field.clean(Some(&value2)),
325			Err(FieldError::Invalid(_))
326		));
327	}
328}