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	/// let is_valid = formset.is_valid();
140	/// ```
141	pub fn is_valid(&mut self) -> bool {
142		self.errors.clear();
143
144		// Validate individual forms
145		let mut all_valid = true;
146		for form in &mut self.forms {
147			if !form.is_valid() {
148				all_valid = false;
149			}
150		}
151
152		// Check minimum number
153		if self.forms.len() < self.min_num {
154			self.errors
155				.push(format!("Please submit at least {} forms", self.min_num));
156			all_valid = false;
157		}
158
159		// Check maximum number
160		if let Some(max) = self.max_num
161			&& self.forms.len() > max
162		{
163			self.errors
164				.push(format!("Please submit no more than {} forms", max));
165			all_valid = false;
166		}
167
168		all_valid && self.errors.is_empty()
169	}
170	/// Returns the formset-level validation errors.
171	pub fn errors(&self) -> &[String] {
172		&self.errors
173	}
174	/// Returns cleaned data from all forms in the set.
175	pub fn cleaned_data(&self) -> Vec<&HashMap<String, serde_json::Value>> {
176		self.forms.iter().map(|f| f.cleaned_data()).collect()
177	}
178	/// Get management form data (for tracking forms in HTML)
179	///
180	/// # Examples
181	///
182	/// ```
183	/// use reinhardt_forms::FormSet;
184	///
185	/// let formset = FormSet::new("form".to_string());
186	/// let data = formset.management_form_data();
187	/// assert!(data.contains_key("form-TOTAL_FORMS"));
188	/// ```
189	pub fn management_form_data(&self) -> HashMap<String, String> {
190		let mut data = HashMap::new();
191		data.insert(
192			format!("{}-TOTAL_FORMS", self.prefix),
193			self.total_form_count().to_string(),
194		);
195		data.insert(
196			format!("{}-INITIAL_FORMS", self.prefix),
197			self.forms.len().to_string(),
198		);
199		data.insert(
200			format!("{}-MIN_NUM_FORMS", self.prefix),
201			self.min_num.to_string(),
202		);
203		if let Some(max) = self.max_num {
204			data.insert(format!("{}-MAX_NUM_FORMS", self.prefix), max.to_string());
205		}
206		data
207	}
208	/// Process bound data from HTML forms.
209	///
210	/// Respects `max_num` and silently stops adding forms once the limit is reached.
211	///
212	/// # Examples
213	///
214	/// ```
215	/// use reinhardt_forms::FormSet;
216	/// use std::collections::HashMap;
217	/// use serde_json::json;
218	///
219	/// let mut formset = FormSet::new("form".to_string());
220	/// let mut data = HashMap::new();
221	/// let mut form_data = HashMap::new();
222	/// form_data.insert("field".to_string(), json!("value"));
223	/// data.insert("form-0".to_string(), form_data);
224	///
225	/// formset.process_data(&data);
226	/// assert_eq!(formset.form_count(), 1);
227	/// ```
228	pub fn process_data(&mut self, data: &HashMap<String, HashMap<String, serde_json::Value>>) {
229		self.forms.clear();
230
231		// Sort keys for deterministic ordering when max_num limit is applied
232		let mut keys: Vec<&String> = data.keys().collect();
233		keys.sort();
234
235		// Each form should have a key like "form-0", "form-1", etc.
236		// Use exact prefix matching with delimiter to prevent collisions
237		// (e.g., prefix "item" should not match "item_extra-0")
238		let prefix_with_delimiter = format!("{}-", self.prefix);
239		for key in keys {
240			if key.starts_with(&prefix_with_delimiter) {
241				// Enforce max_num limit during data processing
242				if let Some(max) = self.max_num
243					&& self.forms.len() >= max
244				{
245					break;
246				}
247				if let Some(form_data) = data.get(key) {
248					let mut form = Form::new();
249					form.bind(form_data.clone());
250					self.forms.push(form);
251				}
252			}
253		}
254	}
255}
256
257impl Default for FormSet {
258	fn default() -> Self {
259		Self::new("form".to_string())
260	}
261}
262
263#[cfg(test)]
264mod tests {
265	use super::*;
266	use crate::fields::CharField;
267	use rstest::rstest;
268
269	#[test]
270	fn test_formset_basic() {
271		let mut formset = FormSet::new("person".to_string());
272
273		let mut form1 = Form::new();
274		form1.add_field(Box::new(CharField::new("name".to_string())));
275
276		let mut form2 = Form::new();
277		form2.add_field(Box::new(CharField::new("name".to_string())));
278
279		formset.add_form(form1).unwrap();
280		formset.add_form(form2).unwrap();
281
282		assert_eq!(formset.form_count(), 2);
283	}
284
285	#[test]
286	fn test_formset_min_num_validation() {
287		let mut formset = FormSet::new("person".to_string()).with_min_num(2);
288
289		let mut form1 = Form::new();
290		form1.add_field(Box::new(CharField::new("name".to_string())));
291		formset.add_form(form1).unwrap();
292
293		assert!(!formset.is_valid());
294		assert!(!formset.errors().is_empty());
295	}
296
297	#[test]
298	fn test_formset_max_num_enforced_on_add() {
299		let mut formset = FormSet::new("person".to_string()).with_max_num(Some(2));
300
301		let mut form1 = Form::new();
302		form1.add_field(Box::new(CharField::new("name".to_string())));
303		assert!(formset.add_form(form1).is_ok());
304
305		let mut form2 = Form::new();
306		form2.add_field(Box::new(CharField::new("name".to_string())));
307		assert!(formset.add_form(form2).is_ok());
308
309		// Third form should be rejected
310		let mut form3 = Form::new();
311		form3.add_field(Box::new(CharField::new("name".to_string())));
312		assert!(formset.add_form(form3).is_err());
313
314		assert_eq!(formset.form_count(), 2);
315	}
316
317	#[rstest]
318	fn test_process_data_basic_two_forms() {
319		// Arrange
320		let mut formset = FormSet::new("form".to_string());
321		let mut data = HashMap::new();
322
323		let mut form0_data = HashMap::new();
324		form0_data.insert("name".to_string(), serde_json::json!("Alice"));
325		data.insert("form-0".to_string(), form0_data);
326
327		let mut form1_data = HashMap::new();
328		form1_data.insert("name".to_string(), serde_json::json!("Bob"));
329		data.insert("form-1".to_string(), form1_data);
330
331		// Act
332		formset.process_data(&data);
333
334		// Assert
335		assert_eq!(formset.form_count(), 2);
336	}
337
338	#[rstest]
339	fn test_process_data_deterministic_ordering() {
340		// Arrange
341		let mut formset = FormSet::new("form".to_string());
342		let mut data = HashMap::new();
343
344		// Insert in reverse order to verify sorting
345		let mut form2_data = HashMap::new();
346		form2_data.insert("name".to_string(), serde_json::json!("Charlie"));
347		data.insert("form-2".to_string(), form2_data);
348
349		let mut form0_data = HashMap::new();
350		form0_data.insert("name".to_string(), serde_json::json!("Alice"));
351		data.insert("form-0".to_string(), form0_data);
352
353		let mut form1_data = HashMap::new();
354		form1_data.insert("name".to_string(), serde_json::json!("Bob"));
355		data.insert("form-1".to_string(), form1_data);
356
357		// Act
358		formset.process_data(&data);
359
360		// Assert
361		assert_eq!(formset.form_count(), 3);
362		let cleaned: Vec<_> = formset.cleaned_data();
363		assert_eq!(cleaned[0].get("name"), Some(&serde_json::json!("Alice")));
364		assert_eq!(cleaned[1].get("name"), Some(&serde_json::json!("Bob")));
365		assert_eq!(cleaned[2].get("name"), Some(&serde_json::json!("Charlie")));
366	}
367
368	#[rstest]
369	fn test_process_data_max_num_constraint() {
370		// Arrange
371		let mut formset = FormSet::new("form".to_string()).with_max_num(Some(2));
372		let mut data = HashMap::new();
373
374		for i in 0..5 {
375			let mut form_data = HashMap::new();
376			form_data.insert("name".to_string(), serde_json::json!(format!("User{}", i)));
377			data.insert(format!("form-{}", i), form_data);
378		}
379
380		// Act
381		formset.process_data(&data);
382
383		// Assert
384		assert_eq!(formset.form_count(), 2);
385	}
386
387	#[rstest]
388	fn test_process_data_prefix_mismatch_keys_ignored() {
389		// Arrange
390		let mut formset = FormSet::new("person".to_string());
391		let mut data = HashMap::new();
392
393		let mut matching = HashMap::new();
394		matching.insert("name".to_string(), serde_json::json!("Alice"));
395		data.insert("person-0".to_string(), matching);
396
397		let mut mismatched = HashMap::new();
398		mismatched.insert("name".to_string(), serde_json::json!("Bob"));
399		data.insert("form-0".to_string(), mismatched);
400
401		// Act
402		formset.process_data(&data);
403
404		// Assert
405		assert_eq!(formset.form_count(), 1);
406		let cleaned = formset.cleaned_data();
407		assert_eq!(cleaned[0].get("name"), Some(&serde_json::json!("Alice")));
408	}
409
410	#[rstest]
411	fn test_process_data_prefix_collision_prevented() {
412		// Arrange: prefix "item" should NOT match "item_extra-0"
413		let mut formset = FormSet::new("item".to_string());
414		let mut data = HashMap::new();
415
416		let mut matching = HashMap::new();
417		matching.insert("name".to_string(), serde_json::json!("Apple"));
418		data.insert("item-0".to_string(), matching);
419
420		let mut colliding = HashMap::new();
421		colliding.insert("name".to_string(), serde_json::json!("Banana"));
422		data.insert("item_extra-0".to_string(), colliding);
423
424		// Act
425		formset.process_data(&data);
426
427		// Assert: only "item-0" should be processed, not "item_extra-0"
428		assert_eq!(formset.form_count(), 1);
429		let cleaned = formset.cleaned_data();
430		assert_eq!(cleaned[0].get("name"), Some(&serde_json::json!("Apple")));
431	}
432
433	#[rstest]
434	fn test_process_data_similar_prefix_no_collision() {
435		// Arrange: prefix "form" should NOT match "formset-0" or "form2-0"
436		let mut formset = FormSet::new("form".to_string());
437		let mut data = HashMap::new();
438
439		let mut matching = HashMap::new();
440		matching.insert("name".to_string(), serde_json::json!("Valid"));
441		data.insert("form-0".to_string(), matching);
442
443		let mut similar1 = HashMap::new();
444		similar1.insert("name".to_string(), serde_json::json!("Invalid1"));
445		data.insert("formset-0".to_string(), similar1);
446
447		let mut similar2 = HashMap::new();
448		similar2.insert("name".to_string(), serde_json::json!("Invalid2"));
449		data.insert("form2-0".to_string(), similar2);
450
451		// Act
452		formset.process_data(&data);
453
454		// Assert: only "form-0" should be processed
455		assert_eq!(formset.form_count(), 1);
456		let cleaned = formset.cleaned_data();
457		assert_eq!(cleaned[0].get("name"), Some(&serde_json::json!("Valid")));
458	}
459
460	#[test]
461	fn test_forms_formset_management_data() {
462		let formset = FormSet::new("person".to_string())
463			.with_extra(3)
464			.with_min_num(1)
465			.with_max_num(Some(10));
466
467		let mgmt_data = formset.management_form_data();
468
469		assert_eq!(mgmt_data.get("person-TOTAL_FORMS"), Some(&"3".to_string()));
470		assert_eq!(
471			mgmt_data.get("person-INITIAL_FORMS"),
472			Some(&"0".to_string())
473		);
474		assert_eq!(
475			mgmt_data.get("person-MIN_NUM_FORMS"),
476			Some(&"1".to_string())
477		);
478		assert_eq!(
479			mgmt_data.get("person-MAX_NUM_FORMS"),
480			Some(&"10".to_string())
481		);
482	}
483}