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		Self {
30			forms: vec![],
31			prefix,
32			can_delete: false,
33			can_order: false,
34			extra: 1,
35			max_num: Some(1000),
36			min_num: 0,
37			errors: vec![],
38		}
39	}
40
41	pub fn prefix(&self) -> &str {
42		&self.prefix
43	}
44
45	pub fn can_delete(&self) -> bool {
46		self.can_delete
47	}
48	pub fn with_extra(mut self, extra: usize) -> Self {
49		self.extra = extra;
50		self
51	}
52	pub fn with_can_delete(mut self, can_delete: bool) -> Self {
53		self.can_delete = can_delete;
54		self
55	}
56	pub fn with_can_order(mut self, can_order: bool) -> Self {
57		self.can_order = can_order;
58		self
59	}
60	pub fn with_max_num(mut self, max_num: Option<usize>) -> Self {
61		self.max_num = max_num;
62		self
63	}
64	pub fn with_min_num(mut self, min_num: usize) -> Self {
65		self.min_num = min_num;
66		self
67	}
68	/// Add a form to the formset.
69	///
70	/// Returns an error if adding the form would exceed `max_num`.
71	///
72	/// # Examples
73	///
74	/// ```
75	/// use reinhardt_forms::{FormSet, Form};
76	///
77	/// let mut formset = FormSet::new("form".to_string());
78	/// let form = Form::new();
79	/// assert!(formset.add_form(form).is_ok());
80	/// assert_eq!(formset.forms().len(), 1);
81	/// ```
82	///
83	/// ```
84	/// use reinhardt_forms::{FormSet, Form};
85	///
86	/// let mut formset = FormSet::new("form".to_string()).with_max_num(Some(1));
87	/// assert!(formset.add_form(Form::new()).is_ok());
88	/// assert!(formset.add_form(Form::new()).is_err());
89	/// ```
90	pub fn add_form(&mut self, form: Form) -> Result<(), String> {
91		if let Some(max) = self.max_num
92			&& self.forms.len() >= max
93		{
94			return Err(format!(
95				"Cannot add form: maximum number of forms ({}) reached",
96				max
97			));
98		}
99		self.forms.push(form);
100		Ok(())
101	}
102	pub fn forms(&self) -> &[Form] {
103		&self.forms
104	}
105	pub fn forms_mut(&mut self) -> &mut Vec<Form> {
106		&mut self.forms
107	}
108	pub fn form_count(&self) -> usize {
109		self.forms.len()
110	}
111	pub fn total_form_count(&self) -> usize {
112		self.forms.len() + self.extra
113	}
114	/// Validate all forms in the formset
115	///
116	/// # Examples
117	///
118	/// ```
119	/// use reinhardt_forms::{FormSet, Form};
120	///
121	/// let mut formset = FormSet::new("form".to_string());
122	/// formset.add_form(Form::new()).unwrap();
123	// Note: is_valid() requires mutable reference
124	// let is_valid = formset.is_valid();
125	/// ```
126	pub fn is_valid(&mut self) -> bool {
127		self.errors.clear();
128
129		// Validate individual forms
130		let mut all_valid = true;
131		for form in &mut self.forms {
132			if !form.is_valid() {
133				all_valid = false;
134			}
135		}
136
137		// Check minimum number
138		if self.forms.len() < self.min_num {
139			self.errors
140				.push(format!("Please submit at least {} forms", self.min_num));
141			all_valid = false;
142		}
143
144		// Check maximum number
145		if let Some(max) = self.max_num
146			&& self.forms.len() > max
147		{
148			self.errors
149				.push(format!("Please submit no more than {} forms", max));
150			all_valid = false;
151		}
152
153		all_valid && self.errors.is_empty()
154	}
155	pub fn errors(&self) -> &[String] {
156		&self.errors
157	}
158	pub fn cleaned_data(&self) -> Vec<&HashMap<String, serde_json::Value>> {
159		self.forms.iter().map(|f| f.cleaned_data()).collect()
160	}
161	/// Get management form data (for tracking forms in HTML)
162	///
163	/// # Examples
164	///
165	/// ```
166	/// use reinhardt_forms::FormSet;
167	///
168	/// let formset = FormSet::new("form".to_string());
169	/// let data = formset.management_form_data();
170	/// assert!(data.contains_key("form-TOTAL_FORMS"));
171	/// ```
172	pub fn management_form_data(&self) -> HashMap<String, String> {
173		let mut data = HashMap::new();
174		data.insert(
175			format!("{}-TOTAL_FORMS", self.prefix),
176			self.total_form_count().to_string(),
177		);
178		data.insert(
179			format!("{}-INITIAL_FORMS", self.prefix),
180			self.forms.len().to_string(),
181		);
182		data.insert(
183			format!("{}-MIN_NUM_FORMS", self.prefix),
184			self.min_num.to_string(),
185		);
186		if let Some(max) = self.max_num {
187			data.insert(format!("{}-MAX_NUM_FORMS", self.prefix), max.to_string());
188		}
189		data
190	}
191	/// Process bound data from HTML forms.
192	///
193	/// Respects `max_num` and silently stops adding forms once the limit is reached.
194	///
195	/// # Examples
196	///
197	/// ```
198	/// use reinhardt_forms::FormSet;
199	/// use std::collections::HashMap;
200	/// use serde_json::json;
201	///
202	/// let mut formset = FormSet::new("form".to_string());
203	/// let mut data = HashMap::new();
204	/// let mut form_data = HashMap::new();
205	/// form_data.insert("field".to_string(), json!("value"));
206	/// data.insert("form-0".to_string(), form_data);
207	///
208	/// formset.process_data(&data);
209	/// assert_eq!(formset.form_count(), 1);
210	/// ```
211	pub fn process_data(&mut self, data: &HashMap<String, HashMap<String, serde_json::Value>>) {
212		self.forms.clear();
213
214		// Each form should have a key like "form-0", "form-1", etc.
215		for (key, form_data) in data {
216			if key.starts_with(&self.prefix) {
217				// Enforce max_num limit during data processing
218				if let Some(max) = self.max_num
219					&& self.forms.len() >= max
220				{
221					break;
222				}
223				let mut form = Form::new();
224				form.bind(form_data.clone());
225				self.forms.push(form);
226			}
227		}
228	}
229}
230
231impl Default for FormSet {
232	fn default() -> Self {
233		Self::new("form".to_string())
234	}
235}
236
237#[cfg(test)]
238mod tests {
239	use super::*;
240	use crate::fields::CharField;
241
242	#[test]
243	fn test_formset_basic() {
244		let mut formset = FormSet::new("person".to_string());
245
246		let mut form1 = Form::new();
247		form1.add_field(Box::new(CharField::new("name".to_string())));
248
249		let mut form2 = Form::new();
250		form2.add_field(Box::new(CharField::new("name".to_string())));
251
252		formset.add_form(form1).unwrap();
253		formset.add_form(form2).unwrap();
254
255		assert_eq!(formset.form_count(), 2);
256	}
257
258	#[test]
259	fn test_formset_min_num_validation() {
260		let mut formset = FormSet::new("person".to_string()).with_min_num(2);
261
262		let mut form1 = Form::new();
263		form1.add_field(Box::new(CharField::new("name".to_string())));
264		formset.add_form(form1).unwrap();
265
266		assert!(!formset.is_valid());
267		assert!(!formset.errors().is_empty());
268	}
269
270	#[test]
271	fn test_formset_max_num_enforced_on_add() {
272		let mut formset = FormSet::new("person".to_string()).with_max_num(Some(2));
273
274		let mut form1 = Form::new();
275		form1.add_field(Box::new(CharField::new("name".to_string())));
276		assert!(formset.add_form(form1).is_ok());
277
278		let mut form2 = Form::new();
279		form2.add_field(Box::new(CharField::new("name".to_string())));
280		assert!(formset.add_form(form2).is_ok());
281
282		// Third form should be rejected
283		let mut form3 = Form::new();
284		form3.add_field(Box::new(CharField::new("name".to_string())));
285		assert!(formset.add_form(form3).is_err());
286
287		assert_eq!(formset.form_count(), 2);
288	}
289
290	#[test]
291	fn test_forms_formset_management_data() {
292		let formset = FormSet::new("person".to_string())
293			.with_extra(3)
294			.with_min_num(1)
295			.with_max_num(Some(10));
296
297		let mgmt_data = formset.management_form_data();
298
299		assert_eq!(mgmt_data.get("person-TOTAL_FORMS"), Some(&"3".to_string()));
300		assert_eq!(
301			mgmt_data.get("person-INITIAL_FORMS"),
302			Some(&"0".to_string())
303		);
304		assert_eq!(
305			mgmt_data.get("person-MIN_NUM_FORMS"),
306			Some(&"1".to_string())
307		);
308		assert_eq!(
309			mgmt_data.get("person-MAX_NUM_FORMS"),
310			Some(&"10".to_string())
311		);
312	}
313}