1use crate::form::Form;
2use std::collections::HashMap;
3
4pub 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 pub fn new(prefix: String) -> Self {
29 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 pub fn prefix(&self) -> &str {
48 &self.prefix
49 }
50
51 pub fn can_delete(&self) -> bool {
53 self.can_delete
54 }
55 pub fn with_extra(mut self, extra: usize) -> Self {
57 self.extra = extra;
58 self
59 }
60 pub fn with_can_delete(mut self, can_delete: bool) -> Self {
62 self.can_delete = can_delete;
63 self
64 }
65 pub fn with_can_order(mut self, can_order: bool) -> Self {
67 self.can_order = can_order;
68 self
69 }
70 pub fn with_max_num(mut self, max_num: Option<usize>) -> Self {
72 self.max_num = max_num;
73 self
74 }
75 pub fn with_min_num(mut self, min_num: usize) -> Self {
77 self.min_num = min_num;
78 self
79 }
80 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 pub fn forms(&self) -> &[Form] {
116 &self.forms
117 }
118 pub fn forms_mut(&mut self) -> &mut Vec<Form> {
120 &mut self.forms
121 }
122 pub fn form_count(&self) -> usize {
124 self.forms.len()
125 }
126 pub fn total_form_count(&self) -> usize {
128 self.forms.len() + self.extra
129 }
130 pub fn is_valid(&mut self) -> bool {
142 self.errors.clear();
143
144 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 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 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 pub fn errors(&self) -> &[String] {
172 &self.errors
173 }
174 pub fn cleaned_data(&self) -> Vec<&HashMap<String, serde_json::Value>> {
176 self.forms.iter().map(|f| f.cleaned_data()).collect()
177 }
178 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 pub fn process_data(&mut self, data: &HashMap<String, HashMap<String, serde_json::Value>>) {
229 self.forms.clear();
230
231 let mut keys: Vec<&String> = data.keys().collect();
233 keys.sort();
234
235 let prefix_with_delimiter = format!("{}-", self.prefix);
239 for key in keys {
240 if key.starts_with(&prefix_with_delimiter) {
241 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 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 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 formset.process_data(&data);
333
334 assert_eq!(formset.form_count(), 2);
336 }
337
338 #[rstest]
339 fn test_process_data_deterministic_ordering() {
340 let mut formset = FormSet::new("form".to_string());
342 let mut data = HashMap::new();
343
344 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 formset.process_data(&data);
359
360 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 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 formset.process_data(&data);
382
383 assert_eq!(formset.form_count(), 2);
385 }
386
387 #[rstest]
388 fn test_process_data_prefix_mismatch_keys_ignored() {
389 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 formset.process_data(&data);
403
404 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 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 formset.process_data(&data);
426
427 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 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 formset.process_data(&data);
453
454 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}