Skip to main content

reinhardt_forms/fields/
choice_field.rs

1use crate::field::{FieldError, FieldResult, FormField, Widget};
2
3/// ChoiceField for selecting from predefined choices
4#[derive(Debug, Clone)]
5pub struct ChoiceField {
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 a selection is required.
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	/// Available choices as (value, display_label) pairs.
19	pub choices: Vec<(String, String)>,
20}
21
22impl ChoiceField {
23	/// Create a new ChoiceField
24	///
25	/// # Examples
26	///
27	/// ```
28	/// use reinhardt_forms::fields::ChoiceField;
29	///
30	/// let choices = vec![("1".to_string(), "Option 1".to_string())];
31	/// let field = ChoiceField::new("choice".to_string(), choices);
32	/// assert_eq!(field.name, "choice");
33	/// ```
34	pub fn new(name: String, choices: Vec<(String, String)>) -> Self {
35		Self {
36			name,
37			label: None,
38			required: true,
39			help_text: None,
40			widget: Widget::Select {
41				choices: choices.clone(),
42			},
43			initial: None,
44			choices,
45		}
46	}
47}
48
49impl FormField for ChoiceField {
50	fn name(&self) -> &str {
51		&self.name
52	}
53
54	fn label(&self) -> Option<&str> {
55		self.label.as_deref()
56	}
57
58	fn required(&self) -> bool {
59		self.required
60	}
61
62	fn help_text(&self) -> Option<&str> {
63		self.help_text.as_deref()
64	}
65
66	fn widget(&self) -> &Widget {
67		&self.widget
68	}
69
70	fn initial(&self) -> Option<&serde_json::Value> {
71		self.initial.as_ref()
72	}
73
74	fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
75		match value {
76			None if self.required => Err(FieldError::required(None)),
77			None => Ok(serde_json::Value::String(String::new())),
78			Some(v) => {
79				let s = v
80					.as_str()
81					.ok_or_else(|| FieldError::Invalid("Expected string".to_string()))?;
82
83				let s = s.trim();
84
85				if s.is_empty() {
86					if self.required {
87						return Err(FieldError::required(None));
88					}
89					return Ok(serde_json::Value::String(String::new()));
90				}
91
92				// Check if value is in choices
93				let valid = self.choices.iter().any(|(value, _)| value == s);
94				if !valid {
95					return Err(FieldError::Validation(format!(
96						"Select a valid choice. '{}' is not one of the available choices",
97						s
98					)));
99				}
100
101				Ok(serde_json::Value::String(s.to_string()))
102			}
103		}
104	}
105}
106
107/// MultipleChoiceField for selecting multiple choices
108#[derive(Debug, Clone)]
109pub struct MultipleChoiceField {
110	/// The field name used as the form data key.
111	pub name: String,
112	/// Optional human-readable label for display.
113	pub label: Option<String>,
114	/// Whether at least one selection is required.
115	pub required: bool,
116	/// Optional help text displayed alongside the field.
117	pub help_text: Option<String>,
118	/// The widget type used for rendering this field.
119	pub widget: Widget,
120	/// Optional initial (default) value for the field.
121	pub initial: Option<serde_json::Value>,
122	/// Available choices as (value, display_label) pairs.
123	pub choices: Vec<(String, String)>,
124}
125
126impl MultipleChoiceField {
127	/// Creates a new `MultipleChoiceField` with the given name and choices.
128	pub fn new(name: String, choices: Vec<(String, String)>) -> Self {
129		Self {
130			name,
131			label: None,
132			required: true,
133			help_text: None,
134			widget: Widget::Select {
135				choices: choices.clone(),
136			},
137			initial: None,
138			choices,
139		}
140	}
141}
142
143impl FormField for MultipleChoiceField {
144	fn name(&self) -> &str {
145		&self.name
146	}
147
148	fn label(&self) -> Option<&str> {
149		self.label.as_deref()
150	}
151
152	fn required(&self) -> bool {
153		self.required
154	}
155
156	fn help_text(&self) -> Option<&str> {
157		self.help_text.as_deref()
158	}
159
160	fn widget(&self) -> &Widget {
161		&self.widget
162	}
163
164	fn initial(&self) -> Option<&serde_json::Value> {
165		self.initial.as_ref()
166	}
167
168	fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
169		match value {
170			None if self.required => Err(FieldError::required(None)),
171			None => Ok(serde_json::json!([])),
172			Some(v) => {
173				let values: Vec<String> = if let Some(arr) = v.as_array() {
174					arr.iter()
175						.filter_map(|v| v.as_str().map(|s| s.to_string()))
176						.collect()
177				} else if let Some(s) = v.as_str() {
178					vec![s.to_string()]
179				} else {
180					return Err(FieldError::Invalid("Expected array or string".to_string()));
181				};
182
183				if values.is_empty() && self.required {
184					return Err(FieldError::required(None));
185				}
186
187				// Validate all values are in choices
188				for val in &values {
189					let valid = self.choices.iter().any(|(choice, _)| choice == val);
190					if !valid {
191						return Err(FieldError::Validation(format!(
192							"Select a valid choice. '{}' is not one of the available choices",
193							val
194						)));
195					}
196				}
197
198				Ok(serde_json::json!(values))
199			}
200		}
201	}
202}
203
204#[cfg(test)]
205mod tests {
206	use super::*;
207
208	#[test]
209	fn test_choicefield_valid() {
210		let choices = vec![
211			("1".to_string(), "One".to_string()),
212			("2".to_string(), "Two".to_string()),
213		];
214		let field = ChoiceField::new("number".to_string(), choices);
215
216		assert_eq!(
217			field.clean(Some(&serde_json::json!("1"))).unwrap(),
218			serde_json::json!("1")
219		);
220	}
221
222	#[test]
223	fn test_choicefield_invalid() {
224		let choices = vec![("1".to_string(), "One".to_string())];
225		let field = ChoiceField::new("number".to_string(), choices);
226
227		assert!(matches!(
228			field.clean(Some(&serde_json::json!("3"))),
229			Err(FieldError::Validation(_))
230		));
231	}
232
233	#[test]
234	fn test_multiplechoicefield() {
235		let choices = vec![
236			("a".to_string(), "A".to_string()),
237			("b".to_string(), "B".to_string()),
238		];
239		let field = MultipleChoiceField::new("letters".to_string(), choices);
240
241		assert_eq!(
242			field.clean(Some(&serde_json::json!(["a", "b"]))).unwrap(),
243			serde_json::json!(["a", "b"])
244		);
245
246		assert!(matches!(
247			field.clean(Some(&serde_json::json!(["a", "c"]))),
248			Err(FieldError::Validation(_))
249		));
250	}
251
252	#[test]
253	fn test_choicefield_required() {
254		let choices = vec![("1".to_string(), "One".to_string())];
255		let field = ChoiceField::new("number".to_string(), choices);
256
257		// Required field rejects None
258		assert!(field.clean(None).is_err());
259
260		// Required field rejects empty string
261		assert!(field.clean(Some(&serde_json::json!(""))).is_err());
262	}
263
264	#[test]
265	fn test_choicefield_not_required() {
266		let choices = vec![("1".to_string(), "One".to_string())];
267		let mut field = ChoiceField::new("number".to_string(), choices);
268		field.required = false;
269
270		// Not required accepts None
271		assert_eq!(field.clean(None).unwrap(), serde_json::json!(""));
272
273		// Not required accepts empty string
274		assert_eq!(
275			field.clean(Some(&serde_json::json!(""))).unwrap(),
276			serde_json::json!("")
277		);
278	}
279
280	#[test]
281	fn test_choicefield_whitespace_trimming() {
282		let choices = vec![("1".to_string(), "One".to_string())];
283		let field = ChoiceField::new("number".to_string(), choices);
284
285		// Whitespace should be trimmed before validation
286		assert_eq!(
287			field.clean(Some(&serde_json::json!("  1  "))).unwrap(),
288			serde_json::json!("1")
289		);
290	}
291
292	#[test]
293	fn test_choicefield_multiple_choices() {
294		let choices = vec![
295			("a".to_string(), "Alpha".to_string()),
296			("b".to_string(), "Beta".to_string()),
297			("c".to_string(), "Gamma".to_string()),
298		];
299		let field = ChoiceField::new("greek".to_string(), choices);
300
301		// All choices should be valid
302		assert!(field.clean(Some(&serde_json::json!("a"))).is_ok());
303		assert!(field.clean(Some(&serde_json::json!("b"))).is_ok());
304		assert!(field.clean(Some(&serde_json::json!("c"))).is_ok());
305
306		// Non-existent choice should fail
307		assert!(field.clean(Some(&serde_json::json!("d"))).is_err());
308	}
309
310	#[test]
311	fn test_choicefield_widget_type() {
312		let choices = vec![("1".to_string(), "One".to_string())];
313		let field = ChoiceField::new("number".to_string(), choices.clone());
314
315		// Widget should be Select with choices
316		match field.widget() {
317			Widget::Select {
318				choices: widget_choices,
319			} => {
320				assert_eq!(widget_choices, &choices);
321			}
322			_ => panic!("Expected Select widget"),
323		}
324	}
325
326	#[test]
327	fn test_choicefield_empty_choices() {
328		let choices: Vec<(String, String)> = vec![];
329		let field = ChoiceField::new("empty".to_string(), choices);
330
331		// Any value should be invalid when choices is empty
332		assert!(matches!(
333			field.clean(Some(&serde_json::json!("anything"))),
334			Err(FieldError::Validation(_))
335		));
336	}
337
338	#[test]
339	fn test_choicefield_case_sensitive() {
340		let choices = vec![("abc".to_string(), "ABC".to_string())];
341		let field = ChoiceField::new("text".to_string(), choices);
342
343		// Exact match should work
344		assert!(field.clean(Some(&serde_json::json!("abc"))).is_ok());
345
346		// Different case should fail (choices are case-sensitive)
347		assert!(matches!(
348			field.clean(Some(&serde_json::json!("ABC"))),
349			Err(FieldError::Validation(_))
350		));
351	}
352
353	#[test]
354	fn test_multiplechoicefield_required() {
355		let choices = vec![("1".to_string(), "One".to_string())];
356		let field = MultipleChoiceField::new("numbers".to_string(), choices);
357
358		// Required field rejects None
359		assert!(field.clean(None).is_err());
360
361		// Required field rejects empty array
362		assert!(field.clean(Some(&serde_json::json!([]))).is_err());
363	}
364
365	#[test]
366	fn test_multiplechoicefield_not_required() {
367		let choices = vec![("1".to_string(), "One".to_string())];
368		let mut field = MultipleChoiceField::new("numbers".to_string(), choices);
369		field.required = false;
370
371		// Not required accepts None
372		assert_eq!(field.clean(None).unwrap(), serde_json::json!([]));
373
374		// Not required accepts empty array
375		assert_eq!(
376			field.clean(Some(&serde_json::json!([]))).unwrap(),
377			serde_json::json!([])
378		);
379	}
380
381	#[test]
382	fn test_multiplechoicefield_single_value() {
383		let choices = vec![
384			("a".to_string(), "A".to_string()),
385			("b".to_string(), "B".to_string()),
386		];
387		let field = MultipleChoiceField::new("letters".to_string(), choices);
388
389		// Single value as string should work
390		assert_eq!(
391			field.clean(Some(&serde_json::json!("a"))).unwrap(),
392			serde_json::json!(["a"])
393		);
394
395		// Invalid single value should fail
396		assert!(matches!(
397			field.clean(Some(&serde_json::json!("z"))),
398			Err(FieldError::Validation(_))
399		));
400	}
401
402	#[test]
403	fn test_multiplechoicefield_multiple_values() {
404		let choices = vec![
405			("1".to_string(), "One".to_string()),
406			("2".to_string(), "Two".to_string()),
407			("3".to_string(), "Three".to_string()),
408		];
409		let field = MultipleChoiceField::new("numbers".to_string(), choices);
410
411		// Valid multiple values
412		assert_eq!(
413			field.clean(Some(&serde_json::json!(["1", "2"]))).unwrap(),
414			serde_json::json!(["1", "2"])
415		);
416
417		assert_eq!(
418			field
419				.clean(Some(&serde_json::json!(["1", "2", "3"])))
420				.unwrap(),
421			serde_json::json!(["1", "2", "3"])
422		);
423
424		// One invalid value should fail entire validation
425		assert!(matches!(
426			field.clean(Some(&serde_json::json!(["1", "2", "4"]))),
427			Err(FieldError::Validation(_))
428		));
429	}
430
431	#[test]
432	fn test_multiplechoicefield_duplicate_values() {
433		let choices = vec![
434			("a".to_string(), "A".to_string()),
435			("b".to_string(), "B".to_string()),
436		];
437		let field = MultipleChoiceField::new("letters".to_string(), choices);
438
439		// Duplicates should be accepted (validation doesn't remove them)
440		let result = field
441			.clean(Some(&serde_json::json!(["a", "a", "b"])))
442			.unwrap();
443		assert_eq!(result, serde_json::json!(["a", "a", "b"]));
444	}
445
446	#[test]
447	fn test_multiplechoicefield_widget_type() {
448		let choices = vec![("1".to_string(), "One".to_string())];
449		let field = MultipleChoiceField::new("numbers".to_string(), choices.clone());
450
451		// Widget should be Select with choices
452		match field.widget() {
453			Widget::Select {
454				choices: widget_choices,
455			} => {
456				assert_eq!(widget_choices, &choices);
457			}
458			_ => panic!("Expected Select widget"),
459		}
460	}
461}