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