Skip to main content

reinhardt_forms/
formset.rs

1use crate::form::Form;
2use std::collections::HashMap;
3
4/// FormSet manages multiple forms
5pub struct FormSet {
6	forms: Vec<Form>,
7	prefix: String,
8	can_delete: bool,
9	can_order: bool,
10	extra: usize,
11	max_num: Option<usize>,
12	min_num: usize,
13	errors: Vec<String>,
14}
15
16impl FormSet {
17	/// Create a new FormSet with the given prefix
18	///
19	/// # Examples
20	///
21	/// ```
22	/// use reinhardt_forms::FormSet;
23	///
24	/// let formset = FormSet::new("form".to_string());
25	/// assert_eq!(formset.prefix(), "form");
26	/// assert!(!formset.can_delete());
27	/// ```
28	pub fn new(prefix: String) -> Self {
29		// Normalize: strip trailing delimiter so that `format!("{}-", prefix)`
30		// in `process_data` never produces a double-dash (e.g. "form--")
31		let prefix = prefix
32			.strip_suffix('-')
33			.map_or(prefix.clone(), |s| s.to_owned());
34		Self {
35			forms: vec![],
36			prefix,
37			can_delete: false,
38			can_order: false,
39			extra: 1,
40			max_num: Some(1000),
41			min_num: 0,
42			errors: vec![],
43		}
44	}
45
46	/// Returns the prefix used for form field naming.
47	pub fn prefix(&self) -> &str {
48		&self.prefix
49	}
50
51	/// Returns whether forms in this set can be deleted.
52	pub fn can_delete(&self) -> bool {
53		self.can_delete
54	}
55	/// Sets the number of extra empty forms to include.
56	pub fn with_extra(mut self, extra: usize) -> Self {
57		self.extra = extra;
58		self
59	}
60	/// Enables or disables form deletion within this set.
61	pub fn with_can_delete(mut self, can_delete: bool) -> Self {
62		self.can_delete = can_delete;
63		self
64	}
65	/// Enables or disables ordering of forms within this set.
66	pub fn with_can_order(mut self, can_order: bool) -> Self {
67		self.can_order = can_order;
68		self
69	}
70	/// Sets the maximum number of forms allowed.
71	pub fn with_max_num(mut self, max_num: Option<usize>) -> Self {
72		self.max_num = max_num;
73		self
74	}
75	/// Sets the minimum number of forms required for validation.
76	pub fn with_min_num(mut self, min_num: usize) -> Self {
77		self.min_num = min_num;
78		self
79	}
80	/// Add a form to the formset.
81	///
82	/// Returns an error if adding the form would exceed `max_num`.
83	///
84	/// # Examples
85	///
86	/// ```
87	/// use reinhardt_forms::{FormSet, Form};
88	///
89	/// let mut formset = FormSet::new("form".to_string());
90	/// let form = Form::new();
91	/// assert!(formset.add_form(form).is_ok());
92	/// assert_eq!(formset.forms().len(), 1);
93	/// ```
94	///
95	/// ```
96	/// use reinhardt_forms::{FormSet, Form};
97	///
98	/// let mut formset = FormSet::new("form".to_string()).with_max_num(Some(1));
99	/// assert!(formset.add_form(Form::new()).is_ok());
100	/// assert!(formset.add_form(Form::new()).is_err());
101	/// ```
102	pub fn add_form(&mut self, form: Form) -> Result<(), String> {
103		if let Some(max) = self.max_num
104			&& self.forms.len() >= max
105		{
106			return Err(format!(
107				"Cannot add form: maximum number of forms ({}) reached",
108				max
109			));
110		}
111		self.forms.push(form);
112		Ok(())
113	}
114	/// Returns a slice of all forms in this set.
115	pub fn forms(&self) -> &[Form] {
116		&self.forms
117	}
118	/// Returns a mutable reference to the forms vector.
119	pub fn forms_mut(&mut self) -> &mut Vec<Form> {
120		&mut self.forms
121	}
122	/// Returns the number of forms currently in this set.
123	pub fn form_count(&self) -> usize {
124		self.forms.len()
125	}
126	/// Returns the total form count including extra empty forms.
127	pub fn total_form_count(&self) -> usize {
128		self.forms.len() + self.extra
129	}
130	/// Validate all forms in the formset
131	///
132	/// # Examples
133	///
134	/// ```
135	/// use reinhardt_forms::{FormSet, Form};
136	///
137	/// let mut formset = FormSet::new("form".to_string());
138	/// formset.add_form(Form::new()).unwrap();
139	// Note: is_valid() requires mutable reference
140	// let is_valid = formset.is_valid();
141	/// ```
142	pub fn is_valid(&mut self) -> bool {
143		self.errors.clear();
144
145		// Validate individual forms
146		let mut all_valid = true;
147		for form in &mut self.forms {
148			if !form.is_valid() {
149				all_valid = false;
150			}
151		}
152
153		// Check minimum number
154		if self.forms.len() < self.min_num {
155			self.errors
156				.push(format!("Please submit at least {} forms", self.min_num));
157			all_valid = false;
158		}
159
160		// Check maximum number
161		if let Some(max) = self.max_num
162			&& self.forms.len() > max
163		{
164			self.errors
165				.push(format!("Please submit no more than {} forms", max));
166			all_valid = false;
167		}
168
169		all_valid && self.errors.is_empty()
170	}
171	/// Returns the formset-level validation errors.
172	pub fn errors(&self) -> &[String] {
173		&self.errors
174	}
175	/// Returns cleaned data from all forms in the set.
176	pub fn cleaned_data(&self) -> Vec<&HashMap<String, serde_json::Value>> {
177		self.forms.iter().map(|f| f.cleaned_data()).collect()
178	}
179	/// Get management form data (for tracking forms in HTML)
180	///
181	/// # Examples
182	///
183	/// ```
184	/// use reinhardt_forms::FormSet;
185	///
186	/// let formset = FormSet::new("form".to_string());
187	/// let data = formset.management_form_data();
188	/// assert!(data.contains_key("form-TOTAL_FORMS"));
189	/// ```
190	pub fn management_form_data(&self) -> HashMap<String, String> {
191		let mut data = HashMap::new();
192		data.insert(
193			format!("{}-TOTAL_FORMS", self.prefix),
194			self.total_form_count().to_string(),
195		);
196		data.insert(
197			format!("{}-INITIAL_FORMS", self.prefix),
198			self.forms.len().to_string(),
199		);
200		data.insert(
201			format!("{}-MIN_NUM_FORMS", self.prefix),
202			self.min_num.to_string(),
203		);
204		if let Some(max) = self.max_num {
205			data.insert(format!("{}-MAX_NUM_FORMS", self.prefix), max.to_string());
206		}
207		data
208	}
209	/// Process bound data from HTML forms.
210	///
211	/// Respects `max_num` and silently stops adding forms once the limit is reached.
212	///
213	/// # Examples
214	///
215	/// ```
216	/// use reinhardt_forms::FormSet;
217	/// use std::collections::HashMap;
218	/// use serde_json::json;
219	///
220	/// let mut formset = FormSet::new("form".to_string());
221	/// let mut data = HashMap::new();
222	/// let mut form_data = HashMap::new();
223	/// form_data.insert("field".to_string(), json!("value"));
224	/// data.insert("form-0".to_string(), form_data);
225	///
226	/// formset.process_data(&data);
227	/// assert_eq!(formset.form_count(), 1);
228	/// ```
229	pub fn process_data(&mut self, data: &HashMap<String, HashMap<String, serde_json::Value>>) {
230		self.forms.clear();
231
232		// Sort keys for deterministic ordering when max_num limit is applied
233		let mut keys: Vec<&String> = data.keys().collect();
234		keys.sort();
235
236		// Each form should have a key like "form-0", "form-1", etc.
237		// Use exact prefix matching with delimiter to prevent collisions
238		// (e.g., prefix "item" should not match "item_extra-0")
239		let prefix_with_delimiter = format!("{}-", self.prefix);
240		for key in keys {
241			if key.starts_with(&prefix_with_delimiter) {
242				// Enforce max_num limit during data processing
243				if let Some(max) = self.max_num
244					&& self.forms.len() >= max
245				{
246					break;
247				}
248				if let Some(form_data) = data.get(key) {
249					let mut form = Form::new();
250					form.bind(form_data.clone());
251					self.forms.push(form);
252				}
253			}
254		}
255	}
256}
257
258impl Default for FormSet {
259	fn default() -> Self {
260		Self::new("form".to_string())
261	}
262}
263
264#[cfg(test)]
265mod tests {
266	use super::*;
267	use crate::fields::CharField;
268	use rstest::rstest;
269
270	#[test]
271	fn test_formset_basic() {
272		let mut formset = FormSet::new("person".to_string());
273
274		let mut form1 = Form::new();
275		form1.add_field(Box::new(CharField::new("name".to_string())));
276
277		let mut form2 = Form::new();
278		form2.add_field(Box::new(CharField::new("name".to_string())));
279
280		formset.add_form(form1).unwrap();
281		formset.add_form(form2).unwrap();
282
283		assert_eq!(formset.form_count(), 2);
284	}
285
286	#[test]
287	fn test_formset_min_num_validation() {
288		let mut formset = FormSet::new("person".to_string()).with_min_num(2);
289
290		let mut form1 = Form::new();
291		form1.add_field(Box::new(CharField::new("name".to_string())));
292		formset.add_form(form1).unwrap();
293
294		assert!(!formset.is_valid());
295		assert!(!formset.errors().is_empty());
296	}
297
298	#[test]
299	fn test_formset_max_num_enforced_on_add() {
300		let mut formset = FormSet::new("person".to_string()).with_max_num(Some(2));
301
302		let mut form1 = Form::new();
303		form1.add_field(Box::new(CharField::new("name".to_string())));
304		assert!(formset.add_form(form1).is_ok());
305
306		let mut form2 = Form::new();
307		form2.add_field(Box::new(CharField::new("name".to_string())));
308		assert!(formset.add_form(form2).is_ok());
309
310		// Third form should be rejected
311		let mut form3 = Form::new();
312		form3.add_field(Box::new(CharField::new("name".to_string())));
313		assert!(formset.add_form(form3).is_err());
314
315		assert_eq!(formset.form_count(), 2);
316	}
317
318	#[rstest]
319	fn test_process_data_basic_two_forms() {
320		// Arrange
321		let mut formset = FormSet::new("form".to_string());
322		let mut data = HashMap::new();
323
324		let mut form0_data = HashMap::new();
325		form0_data.insert("name".to_string(), serde_json::json!("Alice"));
326		data.insert("form-0".to_string(), form0_data);
327
328		let mut form1_data = HashMap::new();
329		form1_data.insert("name".to_string(), serde_json::json!("Bob"));
330		data.insert("form-1".to_string(), form1_data);
331
332		// Act
333		formset.process_data(&data);
334
335		// Assert
336		assert_eq!(formset.form_count(), 2);
337	}
338
339	#[rstest]
340	fn test_process_data_deterministic_ordering() {
341		// Arrange
342		let mut formset = FormSet::new("form".to_string());
343		let mut data = HashMap::new();
344
345		// Insert in reverse order to verify sorting
346		let mut form2_data = HashMap::new();
347		form2_data.insert("name".to_string(), serde_json::json!("Charlie"));
348		data.insert("form-2".to_string(), form2_data);
349
350		let mut form0_data = HashMap::new();
351		form0_data.insert("name".to_string(), serde_json::json!("Alice"));
352		data.insert("form-0".to_string(), form0_data);
353
354		let mut form1_data = HashMap::new();
355		form1_data.insert("name".to_string(), serde_json::json!("Bob"));
356		data.insert("form-1".to_string(), form1_data);
357
358		// Act
359		formset.process_data(&data);
360
361		// Assert
362		assert_eq!(formset.form_count(), 3);
363		let cleaned: Vec<_> = formset.cleaned_data();
364		assert_eq!(cleaned[0].get("name"), Some(&serde_json::json!("Alice")));
365		assert_eq!(cleaned[1].get("name"), Some(&serde_json::json!("Bob")));
366		assert_eq!(cleaned[2].get("name"), Some(&serde_json::json!("Charlie")));
367	}
368
369	#[rstest]
370	fn test_process_data_max_num_constraint() {
371		// Arrange
372		let mut formset = FormSet::new("form".to_string()).with_max_num(Some(2));
373		let mut data = HashMap::new();
374
375		for i in 0..5 {
376			let mut form_data = HashMap::new();
377			form_data.insert("name".to_string(), serde_json::json!(format!("User{}", i)));
378			data.insert(format!("form-{}", i), form_data);
379		}
380
381		// Act
382		formset.process_data(&data);
383
384		// Assert
385		assert_eq!(formset.form_count(), 2);
386	}
387
388	#[rstest]
389	fn test_process_data_prefix_mismatch_keys_ignored() {
390		// Arrange
391		let mut formset = FormSet::new("person".to_string());
392		let mut data = HashMap::new();
393
394		let mut matching = HashMap::new();
395		matching.insert("name".to_string(), serde_json::json!("Alice"));
396		data.insert("person-0".to_string(), matching);
397
398		let mut mismatched = HashMap::new();
399		mismatched.insert("name".to_string(), serde_json::json!("Bob"));
400		data.insert("form-0".to_string(), mismatched);
401
402		// Act
403		formset.process_data(&data);
404
405		// Assert
406		assert_eq!(formset.form_count(), 1);
407		let cleaned = formset.cleaned_data();
408		assert_eq!(cleaned[0].get("name"), Some(&serde_json::json!("Alice")));
409	}
410
411	#[rstest]
412	fn test_process_data_prefix_collision_prevented() {
413		// Arrange: prefix "item" should NOT match "item_extra-0"
414		let mut formset = FormSet::new("item".to_string());
415		let mut data = HashMap::new();
416
417		let mut matching = HashMap::new();
418		matching.insert("name".to_string(), serde_json::json!("Apple"));
419		data.insert("item-0".to_string(), matching);
420
421		let mut colliding = HashMap::new();
422		colliding.insert("name".to_string(), serde_json::json!("Banana"));
423		data.insert("item_extra-0".to_string(), colliding);
424
425		// Act
426		formset.process_data(&data);
427
428		// Assert: only "item-0" should be processed, not "item_extra-0"
429		assert_eq!(formset.form_count(), 1);
430		let cleaned = formset.cleaned_data();
431		assert_eq!(cleaned[0].get("name"), Some(&serde_json::json!("Apple")));
432	}
433
434	#[rstest]
435	fn test_process_data_similar_prefix_no_collision() {
436		// Arrange: prefix "form" should NOT match "formset-0" or "form2-0"
437		let mut formset = FormSet::new("form".to_string());
438		let mut data = HashMap::new();
439
440		let mut matching = HashMap::new();
441		matching.insert("name".to_string(), serde_json::json!("Valid"));
442		data.insert("form-0".to_string(), matching);
443
444		let mut similar1 = HashMap::new();
445		similar1.insert("name".to_string(), serde_json::json!("Invalid1"));
446		data.insert("formset-0".to_string(), similar1);
447
448		let mut similar2 = HashMap::new();
449		similar2.insert("name".to_string(), serde_json::json!("Invalid2"));
450		data.insert("form2-0".to_string(), similar2);
451
452		// Act
453		formset.process_data(&data);
454
455		// Assert: only "form-0" should be processed
456		assert_eq!(formset.form_count(), 1);
457		let cleaned = formset.cleaned_data();
458		assert_eq!(cleaned[0].get("name"), Some(&serde_json::json!("Valid")));
459	}
460
461	#[test]
462	fn test_forms_formset_management_data() {
463		let formset = FormSet::new("person".to_string())
464			.with_extra(3)
465			.with_min_num(1)
466			.with_max_num(Some(10));
467
468		let mgmt_data = formset.management_form_data();
469
470		assert_eq!(mgmt_data.get("person-TOTAL_FORMS"), Some(&"3".to_string()));
471		assert_eq!(
472			mgmt_data.get("person-INITIAL_FORMS"),
473			Some(&"0".to_string())
474		);
475		assert_eq!(
476			mgmt_data.get("person-MIN_NUM_FORMS"),
477			Some(&"1".to_string())
478		);
479		assert_eq!(
480			mgmt_data.get("person-MAX_NUM_FORMS"),
481			Some(&"10".to_string())
482		);
483	}
484}