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