Skip to main content

reinhardt_forms/
form.rs

1use crate::bound_field::BoundField;
2use crate::field::{FieldError, FormField};
3use crate::wasm_compat::ValidationRule;
4use std::collections::HashMap;
5use std::ops::Index;
6
7/// Constant-time byte comparison to prevent timing attacks on CSRF tokens.
8fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
9	if a.len() != b.len() {
10		return false;
11	}
12	let mut result = 0u8;
13	for (x, y) in a.iter().zip(b.iter()) {
14		result |= x ^ y;
15	}
16	result == 0
17}
18
19#[derive(Debug, thiserror::Error)]
20pub enum FormError {
21	#[error("Field error in {field}: {error}")]
22	Field { field: String, error: FieldError },
23	#[error("Validation error: {0}")]
24	Validation(String),
25	#[error("No model instance available for save operation")]
26	NoInstance,
27}
28
29pub type FormResult<T> = Result<T, FormError>;
30
31type CleanFunction =
32	Box<dyn Fn(&HashMap<String, serde_json::Value>) -> FormResult<()> + Send + Sync>;
33type FieldCleanFunction =
34	Box<dyn Fn(&serde_json::Value) -> FormResult<serde_json::Value> + Send + Sync>;
35
36/// Special key for form-level (non-field-specific) errors.
37///
38/// In Django, this is `"__all__"`, but in Rust we use a single underscore
39/// to follow Rust conventions for internal/private identifiers.
40pub const ALL_FIELDS_KEY: &str = "_all";
41
42/// Form data structure (Phase 2-A: Enhanced with client-side validation rules)
43pub struct Form {
44	fields: Vec<Box<dyn FormField>>,
45	data: HashMap<String, serde_json::Value>,
46	initial: HashMap<String, serde_json::Value>,
47	errors: HashMap<String, Vec<String>>,
48	is_bound: bool,
49	clean_functions: Vec<CleanFunction>,
50	field_clean_functions: HashMap<String, FieldCleanFunction>,
51	prefix: String,
52	/// Client-side validation rules (Phase 2-A)
53	/// These rules are transmitted to the client for UX enhancement.
54	/// Server-side validation is still mandatory for security.
55	validation_rules: Vec<ValidationRule>,
56	/// Expected CSRF token for form validation
57	csrf_token: Option<String>,
58	/// Whether CSRF validation is enabled
59	csrf_enabled: bool,
60}
61
62impl Form {
63	/// Create a new empty form
64	///
65	/// # Examples
66	///
67	/// ```
68	/// use reinhardt_forms::Form;
69	///
70	/// let form = Form::new();
71	/// assert!(!form.is_bound());
72	/// assert!(form.fields().is_empty());
73	/// ```
74	pub fn new() -> Self {
75		Self {
76			fields: vec![],
77			data: HashMap::new(),
78			initial: HashMap::new(),
79			errors: HashMap::new(),
80			is_bound: false,
81			clean_functions: vec![],
82			field_clean_functions: HashMap::new(),
83			prefix: String::new(),
84			validation_rules: vec![],
85			csrf_token: None,
86			csrf_enabled: false,
87		}
88	}
89	/// Create a new form with initial data
90	///
91	/// # Examples
92	///
93	/// ```
94	/// use reinhardt_forms::Form;
95	/// use std::collections::HashMap;
96	/// use serde_json::json;
97	///
98	/// let mut initial = HashMap::new();
99	/// initial.insert("name".to_string(), json!("John"));
100	///
101	/// let form = Form::with_initial(initial);
102	/// assert_eq!(form.initial().get("name"), Some(&json!("John")));
103	/// ```
104	pub fn with_initial(initial: HashMap<String, serde_json::Value>) -> Self {
105		Self {
106			fields: vec![],
107			data: HashMap::new(),
108			initial,
109			errors: HashMap::new(),
110			is_bound: false,
111			clean_functions: vec![],
112			field_clean_functions: HashMap::new(),
113			prefix: String::new(),
114			validation_rules: vec![],
115			csrf_token: None,
116			csrf_enabled: false,
117		}
118	}
119	/// Create a new form with a field prefix
120	///
121	/// # Examples
122	///
123	/// ```
124	/// use reinhardt_forms::Form;
125	///
126	/// let form = Form::with_prefix("user".to_string());
127	/// assert_eq!(form.prefix(), "user");
128	/// assert_eq!(form.add_prefix_to_field_name("email"), "user-email");
129	/// ```
130	pub fn with_prefix(prefix: String) -> Self {
131		Self {
132			fields: vec![],
133			data: HashMap::new(),
134			initial: HashMap::new(),
135			errors: HashMap::new(),
136			is_bound: false,
137			clean_functions: vec![],
138			field_clean_functions: HashMap::new(),
139			prefix,
140			validation_rules: vec![],
141			csrf_token: None,
142			csrf_enabled: false,
143		}
144	}
145	/// Add a field to the form
146	///
147	/// # Examples
148	///
149	/// ```
150	/// use reinhardt_forms::{Form, CharField, Field};
151	///
152	/// let mut form = Form::new();
153	/// let field = CharField::new("username".to_string());
154	/// form.add_field(Box::new(field));
155	/// assert_eq!(form.fields().len(), 1);
156	/// ```
157	pub fn add_field(&mut self, field: Box<dyn FormField>) {
158		self.fields.push(field);
159	}
160	/// Bind form data for validation
161	///
162	/// # Examples
163	///
164	/// ```
165	/// use reinhardt_forms::Form;
166	/// use std::collections::HashMap;
167	/// use serde_json::json;
168	///
169	/// let mut form = Form::new();
170	/// let mut data = HashMap::new();
171	/// data.insert("username".to_string(), json!("john"));
172	///
173	/// form.bind(data);
174	/// assert!(form.is_bound());
175	/// ```
176	pub fn bind(&mut self, data: HashMap<String, serde_json::Value>) {
177		self.data = data;
178		self.is_bound = true;
179	}
180	/// Validate the form and return true if all fields are valid
181	///
182	/// # Examples
183	///
184	/// ```
185	/// use reinhardt_forms::{Form, CharField, Field};
186	/// use std::collections::HashMap;
187	/// use serde_json::json;
188	///
189	/// let mut form = Form::new();
190	/// form.add_field(Box::new(CharField::new("username".to_string())));
191	///
192	/// let mut data = HashMap::new();
193	/// data.insert("username".to_string(), json!("john"));
194	/// form.bind(data);
195	///
196	/// assert!(form.is_valid());
197	/// assert!(form.errors().is_empty());
198	/// assert_eq!(form.cleaned_data().get("username"), Some(&json!("john")));
199	/// ```
200	pub fn is_valid(&mut self) -> bool {
201		if !self.is_bound {
202			return false;
203		}
204
205		self.errors.clear();
206
207		// Validate CSRF token if enabled
208		if !self.validate_csrf() {
209			self.errors
210				.entry(ALL_FIELDS_KEY.to_string())
211				.or_default()
212				.push("CSRF token missing or incorrect.".to_string());
213			return false;
214		}
215
216		for field in &self.fields {
217			let value = self.data.get(field.name());
218
219			match field.clean(value) {
220				Ok(mut cleaned) => {
221					// Run field-specific clean function if exists
222					if let Some(field_clean) = self.field_clean_functions.get(field.name()) {
223						match field_clean(&cleaned) {
224							Ok(further_cleaned) => {
225								cleaned = further_cleaned;
226							}
227							Err(e) => {
228								self.errors
229									.entry(field.name().to_string())
230									.or_default()
231									.push(e.to_string());
232								continue;
233							}
234						}
235					}
236					self.data.insert(field.name().to_string(), cleaned);
237				}
238				Err(e) => {
239					self.errors
240						.entry(field.name().to_string())
241						.or_default()
242						.push(e.to_string());
243				}
244			}
245		}
246
247		// Run custom clean functions
248		for clean_fn in &self.clean_functions {
249			if let Err(e) = clean_fn(&self.data) {
250				match e {
251					FormError::Field { field, error } => {
252						self.errors
253							.entry(field)
254							.or_default()
255							.push(error.to_string());
256					}
257					FormError::Validation(msg) => {
258						self.errors
259							.entry(ALL_FIELDS_KEY.to_string())
260							.or_default()
261							.push(msg);
262					}
263					FormError::NoInstance => {
264						self.errors
265							.entry(ALL_FIELDS_KEY.to_string())
266							.or_default()
267							.push(e.to_string());
268					}
269				}
270			}
271		}
272
273		self.errors.is_empty()
274	}
275	pub fn cleaned_data(&self) -> &HashMap<String, serde_json::Value> {
276		&self.data
277	}
278	pub fn errors(&self) -> &HashMap<String, Vec<String>> {
279		&self.errors
280	}
281	pub fn is_bound(&self) -> bool {
282		self.is_bound
283	}
284	pub fn fields(&self) -> &[Box<dyn FormField>] {
285		&self.fields
286	}
287	pub fn initial(&self) -> &HashMap<String, serde_json::Value> {
288		&self.initial
289	}
290	/// Set initial data for the form
291	///
292	/// # Examples
293	///
294	/// ```
295	/// use reinhardt_forms::Form;
296	/// use std::collections::HashMap;
297	/// use serde_json::json;
298	///
299	/// let mut form = Form::new();
300	/// let mut initial = HashMap::new();
301	/// initial.insert("name".to_string(), json!("John"));
302	/// form.set_initial(initial);
303	/// ```
304	pub fn set_initial(&mut self, initial: HashMap<String, serde_json::Value>) {
305		self.initial = initial;
306	}
307	/// Check if any field has changed from its initial value
308	///
309	/// # Examples
310	///
311	/// ```
312	/// use reinhardt_forms::{Form, CharField, Field};
313	/// use std::collections::HashMap;
314	/// use serde_json::json;
315	///
316	/// let mut initial = HashMap::new();
317	/// initial.insert("name".to_string(), json!("John"));
318	///
319	/// let mut form = Form::with_initial(initial);
320	/// form.add_field(Box::new(CharField::new("name".to_string())));
321	///
322	/// let mut data = HashMap::new();
323	/// data.insert("name".to_string(), json!("Jane"));
324	/// form.bind(data);
325	///
326	/// assert!(form.has_changed());
327	/// ```
328	pub fn has_changed(&self) -> bool {
329		if !self.is_bound {
330			return false;
331		}
332
333		for field in &self.fields {
334			let initial_val = self.initial.get(field.name());
335			let data_val = self.data.get(field.name());
336			if field.has_changed(initial_val, data_val) {
337				return true;
338			}
339		}
340		false
341	}
342	pub fn get_field(&self, name: &str) -> Option<&dyn FormField> {
343		self.fields
344			.iter()
345			.find(|f| f.name() == name)
346			.map(|f| f.as_ref())
347	}
348	pub fn remove_field(&mut self, name: &str) -> Option<Box<dyn FormField>> {
349		let pos = self.fields.iter().position(|f| f.name() == name)?;
350		Some(self.fields.remove(pos))
351	}
352	pub fn field_count(&self) -> usize {
353		self.fields.len()
354	}
355	/// Add a custom clean function for form validation
356	///
357	/// # Examples
358	///
359	/// ```
360	/// use reinhardt_forms::Form;
361	/// use std::collections::HashMap;
362	/// use serde_json::json;
363	///
364	/// let mut form = Form::new();
365	/// form.add_clean_function(|data| {
366	///     if data.get("password") != data.get("confirm_password") {
367	///         Err(reinhardt_forms::FormError::Validation("Passwords do not match".to_string()))
368	///     } else {
369	///         Ok(())
370	///     }
371	/// });
372	/// ```
373	pub fn add_clean_function<F>(&mut self, f: F)
374	where
375		F: Fn(&HashMap<String, serde_json::Value>) -> FormResult<()> + Send + Sync + 'static,
376	{
377		self.clean_functions.push(Box::new(f));
378	}
379	/// Add a custom clean function for a specific field
380	///
381	/// # Examples
382	///
383	/// ```
384	/// use reinhardt_forms::Form;
385	/// use serde_json::json;
386	///
387	/// let mut form = Form::new();
388	/// form.add_field_clean_function("email", |value| {
389	///     if let Some(email) = value.as_str() {
390	///         if email.contains("@") {
391	///             Ok(value.clone())
392	///         } else {
393	///             Err(reinhardt_forms::FormError::Validation("Invalid email".to_string()))
394	///         }
395	///     } else {
396	///         Ok(value.clone())
397	///     }
398	/// });
399	/// ```
400	pub fn add_field_clean_function<F>(&mut self, field_name: &str, f: F)
401	where
402		F: Fn(&serde_json::Value) -> FormResult<serde_json::Value> + Send + Sync + 'static,
403	{
404		self.field_clean_functions
405			.insert(field_name.to_string(), Box::new(f));
406	}
407
408	/// Get client-side validation rules (Phase 2-A)
409	///
410	/// # Returns
411	///
412	/// Reference to the validation rules vector
413	pub fn validation_rules(&self) -> &[ValidationRule] {
414		&self.validation_rules
415	}
416
417	/// Add a minimum length validator (Phase 2-A)
418	///
419	/// Adds a validator that checks if a string field has at least `min` characters.
420	/// This validator is executed on the client-side for immediate feedback.
421	///
422	/// **Security Note**: Client-side validation is for UX enhancement only.
423	/// Server-side validation is still mandatory for security.
424	///
425	/// # Arguments
426	///
427	/// - `field_name`: Name of the field to validate
428	/// - `min`: Minimum required length
429	/// - `error_message`: Error message to display on validation failure
430	///
431	/// # Examples
432	///
433	/// ```
434	/// use reinhardt_forms::Form;
435	///
436	/// let mut form = Form::new();
437	/// form.add_min_length_validator("password", 8, "Password must be at least 8 characters");
438	/// ```
439	pub fn add_min_length_validator(
440		&mut self,
441		field_name: impl Into<String>,
442		min: usize,
443		error_message: impl Into<String>,
444	) {
445		self.validation_rules.push(ValidationRule::MinLength {
446			field_name: field_name.into(),
447			min,
448			error_message: error_message.into(),
449		});
450	}
451
452	/// Add a maximum length validator (Phase 2-A)
453	///
454	/// Adds a validator that checks if a string field has at most `max` characters.
455	///
456	/// # Examples
457	///
458	/// ```
459	/// use reinhardt_forms::Form;
460	///
461	/// let mut form = Form::new();
462	/// form.add_max_length_validator("username", 50, "Username must be at most 50 characters");
463	/// ```
464	pub fn add_max_length_validator(
465		&mut self,
466		field_name: impl Into<String>,
467		max: usize,
468		error_message: impl Into<String>,
469	) {
470		self.validation_rules.push(ValidationRule::MaxLength {
471			field_name: field_name.into(),
472			max,
473			error_message: error_message.into(),
474		});
475	}
476
477	/// Add a pattern validator (Phase 2-A)
478	///
479	/// Adds a validator that checks if a string field matches a regex pattern.
480	///
481	/// # Examples
482	///
483	/// ```
484	/// use reinhardt_forms::Form;
485	///
486	/// let mut form = Form::new();
487	/// form.add_pattern_validator("code", "^[A-Z]{3}$", "Code must be 3 uppercase letters");
488	/// ```
489	pub fn add_pattern_validator(
490		&mut self,
491		field_name: impl Into<String>,
492		pattern: impl Into<String>,
493		error_message: impl Into<String>,
494	) {
495		self.validation_rules.push(ValidationRule::Pattern {
496			field_name: field_name.into(),
497			pattern: pattern.into(),
498			error_message: error_message.into(),
499		});
500	}
501
502	/// Add a minimum value validator (Phase 2-A)
503	///
504	/// Adds a validator that checks if a numeric field is at least `min`.
505	///
506	/// # Examples
507	///
508	/// ```
509	/// use reinhardt_forms::Form;
510	///
511	/// let mut form = Form::new();
512	/// form.add_min_value_validator("age", 0.0, "Age must be non-negative");
513	/// ```
514	pub fn add_min_value_validator(
515		&mut self,
516		field_name: impl Into<String>,
517		min: f64,
518		error_message: impl Into<String>,
519	) {
520		self.validation_rules.push(ValidationRule::MinValue {
521			field_name: field_name.into(),
522			min,
523			error_message: error_message.into(),
524		});
525	}
526
527	/// Add a maximum value validator (Phase 2-A)
528	///
529	/// Adds a validator that checks if a numeric field is at most `max`.
530	///
531	/// # Examples
532	///
533	/// ```
534	/// use reinhardt_forms::Form;
535	///
536	/// let mut form = Form::new();
537	/// form.add_max_value_validator("age", 150.0, "Age must be at most 150");
538	/// ```
539	pub fn add_max_value_validator(
540		&mut self,
541		field_name: impl Into<String>,
542		max: f64,
543		error_message: impl Into<String>,
544	) {
545		self.validation_rules.push(ValidationRule::MaxValue {
546			field_name: field_name.into(),
547			max,
548			error_message: error_message.into(),
549		});
550	}
551
552	/// Add an email format validator (Phase 2-A)
553	///
554	/// Adds a validator that checks if a field contains a valid email format.
555	///
556	/// # Examples
557	///
558	/// ```
559	/// use reinhardt_forms::Form;
560	///
561	/// let mut form = Form::new();
562	/// form.add_email_validator("email", "Enter a valid email address");
563	/// ```
564	pub fn add_email_validator(
565		&mut self,
566		field_name: impl Into<String>,
567		error_message: impl Into<String>,
568	) {
569		self.validation_rules.push(ValidationRule::Email {
570			field_name: field_name.into(),
571			error_message: error_message.into(),
572		});
573	}
574
575	/// Add a URL format validator (Phase 2-A)
576	///
577	/// Adds a validator that checks if a field contains a valid URL format.
578	///
579	/// # Examples
580	///
581	/// ```
582	/// use reinhardt_forms::Form;
583	///
584	/// let mut form = Form::new();
585	/// form.add_url_validator("website", "Enter a valid URL");
586	/// ```
587	pub fn add_url_validator(
588		&mut self,
589		field_name: impl Into<String>,
590		error_message: impl Into<String>,
591	) {
592		self.validation_rules.push(ValidationRule::Url {
593			field_name: field_name.into(),
594			error_message: error_message.into(),
595		});
596	}
597
598	/// Add a fields equality validator (Phase 2-A)
599	///
600	/// Adds a validator that checks if multiple fields have equal values.
601	/// Commonly used for password confirmation.
602	///
603	/// # Arguments
604	///
605	/// - `field_names`: Names of fields to compare for equality
606	/// - `error_message`: Error message to display on validation failure
607	/// - `target_field`: Target field for error display (None = non-field error)
608	///
609	/// # Examples
610	///
611	/// ```
612	/// use reinhardt_forms::Form;
613	///
614	/// let mut form = Form::new();
615	/// form.add_fields_equal_validator(
616	///     vec!["password".to_string(), "password_confirm".to_string()],
617	///     "Passwords do not match",
618	///     Some("password_confirm".to_string())
619	/// );
620	/// ```
621	pub fn add_fields_equal_validator(
622		&mut self,
623		field_names: Vec<String>,
624		error_message: impl Into<String>,
625		target_field: Option<String>,
626	) {
627		self.validation_rules.push(ValidationRule::FieldsEqual {
628			field_names,
629			error_message: error_message.into(),
630			target_field,
631		});
632	}
633
634	/// Add a client-side validator reference (Phase 2-A)
635	///
636	/// Adds a reference to a reinhardt-validators Validator.
637	/// This validator is executed on the client-side for immediate feedback.
638	///
639	/// **Security Note**: Client-side validation is for UX enhancement only.
640	/// Server-side validation is still mandatory for security.
641	///
642	/// # Arguments
643	///
644	/// - `field_name`: Name of the field to validate
645	/// - `validator_id`: Validator identifier (e.g., "email", "url", "min_length")
646	/// - `params`: Validator parameters as JSON
647	/// - `error_message`: Error message to display on validation failure
648	///
649	/// # Examples
650	///
651	/// ```
652	/// use reinhardt_forms::Form;
653	/// use serde_json::json;
654	///
655	/// let mut form = Form::new();
656	/// form.add_validator_rule(
657	///     "email",
658	///     "email",
659	///     json!({}),
660	///     "Enter a valid email address"
661	/// );
662	///
663	/// form.add_validator_rule(
664	///     "username",
665	///     "min_length",
666	///     json!({"min": 3}),
667	///     "Username must be at least 3 characters"
668	/// );
669	/// ```
670	pub fn add_validator_rule(
671		&mut self,
672		field_name: impl Into<String>,
673		validator_id: impl Into<String>,
674		params: serde_json::Value,
675		error_message: impl Into<String>,
676	) {
677		self.validation_rules.push(ValidationRule::ValidatorRef {
678			field_name: field_name.into(),
679			validator_id: validator_id.into(),
680			params,
681			error_message: error_message.into(),
682		});
683	}
684
685	/// Helper: Add a date range validator (Phase 2-A)
686	///
687	/// Adds a validator that checks if end_date >= start_date.
688	///
689	/// # Arguments
690	///
691	/// - `start_field`: Name of the start date field
692	/// - `end_field`: Name of the end date field
693	/// - `error_message`: Error message (optional, defaults to a standard message)
694	///
695	/// # Examples
696	///
697	/// ```
698	/// use reinhardt_forms::Form;
699	///
700	/// let mut form = Form::new();
701	/// form.add_date_range_validator("start_date", "end_date", None);
702	/// ```
703	pub fn add_date_range_validator(
704		&mut self,
705		start_field: impl Into<String>,
706		end_field: impl Into<String>,
707		error_message: Option<String>,
708	) {
709		let start = start_field.into();
710		let end = end_field.into();
711		let message = error_message
712			.unwrap_or_else(|| "End date must be after or equal to start date".to_string());
713
714		self.validation_rules.push(ValidationRule::DateRange {
715			start_field: start,
716			end_field: end.clone(),
717			error_message: message,
718			target_field: Some(end),
719		});
720	}
721
722	/// Helper: Add a numeric range validator (Phase 2-A)
723	///
724	/// Adds a validator that checks if max >= min.
725	///
726	/// # Arguments
727	///
728	/// - `min_field`: Name of the minimum value field
729	/// - `max_field`: Name of the maximum value field
730	/// - `error_message`: Error message (optional, defaults to a standard message)
731	///
732	/// # Examples
733	///
734	/// ```
735	/// use reinhardt_forms::Form;
736	///
737	/// let mut form = Form::new();
738	/// form.add_numeric_range_validator("min_price", "max_price", None);
739	/// ```
740	pub fn add_numeric_range_validator(
741		&mut self,
742		min_field: impl Into<String>,
743		max_field: impl Into<String>,
744		error_message: Option<String>,
745	) {
746		let min = min_field.into();
747		let max = max_field.into();
748		let message = error_message.unwrap_or_else(|| {
749			"Maximum value must be greater than or equal to minimum value".to_string()
750		});
751
752		self.validation_rules.push(ValidationRule::NumericRange {
753			min_field: min,
754			max_field: max.clone(),
755			error_message: message,
756			target_field: Some(max),
757		});
758	}
759	/// Enable CSRF protection for this form.
760	///
761	/// When enabled, `is_valid()` will check that the submitted data
762	/// contains a matching CSRF token.
763	///
764	/// # Arguments
765	///
766	/// * `token` - The expected CSRF token for this form
767	///
768	/// # Examples
769	///
770	/// ```
771	/// use reinhardt_forms::Form;
772	///
773	/// let mut form = Form::new();
774	/// form.set_csrf_token("abc123".to_string());
775	/// assert!(form.csrf_enabled());
776	/// ```
777	pub fn set_csrf_token(&mut self, token: String) {
778		self.csrf_token = Some(token);
779		self.csrf_enabled = true;
780	}
781
782	/// Check if CSRF protection is enabled
783	pub fn csrf_enabled(&self) -> bool {
784		self.csrf_enabled
785	}
786
787	/// Get the CSRF token, if set
788	pub fn csrf_token(&self) -> Option<&str> {
789		self.csrf_token.as_deref()
790	}
791
792	/// Validate the submitted CSRF token against the expected token.
793	///
794	/// Returns `true` if CSRF is disabled or the token matches.
795	fn validate_csrf(&self) -> bool {
796		if !self.csrf_enabled {
797			return true;
798		}
799
800		let expected = match &self.csrf_token {
801			Some(t) => t,
802			None => return false,
803		};
804
805		let submitted = self
806			.data
807			.get("csrfmiddlewaretoken")
808			.and_then(|v| v.as_str());
809
810		match submitted {
811			Some(token) => {
812				// Use constant-time comparison to prevent timing attacks
813				constant_time_eq(token.as_bytes(), expected.as_bytes())
814			}
815			None => false,
816		}
817	}
818
819	pub fn prefix(&self) -> &str {
820		&self.prefix
821	}
822	pub fn set_prefix(&mut self, prefix: String) {
823		self.prefix = prefix;
824	}
825	pub fn add_prefix_to_field_name(&self, field_name: &str) -> String {
826		if self.prefix.is_empty() {
827			field_name.to_string()
828		} else {
829			format!("{}-{}", self.prefix, field_name)
830		}
831	}
832	/// Render CSS `<link>` tags for form media with HTML-escaped paths.
833	///
834	/// All paths are escaped using `escape_attribute()` to prevent XSS
835	/// via malicious CSS file paths.
836	///
837	/// # Arguments
838	///
839	/// * `css_files` - Slice of CSS file paths to include
840	///
841	/// # Examples
842	///
843	/// ```
844	/// use reinhardt_forms::Form;
845	///
846	/// let form = Form::new();
847	/// let html = form.render_css_media(&["/static/forms.css"]);
848	/// assert!(html.contains("href=\"/static/forms.css\""));
849	/// ```
850	pub fn render_css_media(&self, css_files: &[&str]) -> String {
851		use crate::field::escape_attribute;
852		let mut html = String::new();
853		for path in css_files {
854			html.push_str(&format!(
855				"<link rel=\"stylesheet\" href=\"{}\" />\n",
856				escape_attribute(path)
857			));
858		}
859		html
860	}
861
862	/// Render JS `<script>` tags for form media with HTML-escaped paths.
863	///
864	/// All paths are escaped using `escape_attribute()` to prevent XSS
865	/// via malicious JS file paths.
866	///
867	/// # Arguments
868	///
869	/// * `js_files` - Slice of JS file paths to include
870	///
871	/// # Examples
872	///
873	/// ```
874	/// use reinhardt_forms::Form;
875	///
876	/// let form = Form::new();
877	/// let html = form.render_js_media(&["/static/forms.js"]);
878	/// assert!(html.contains("src=\"/static/forms.js\""));
879	/// ```
880	pub fn render_js_media(&self, js_files: &[&str]) -> String {
881		use crate::field::escape_attribute;
882		let mut html = String::new();
883		for path in js_files {
884			html.push_str(&format!(
885				"<script src=\"{}\"></script>\n",
886				escape_attribute(path)
887			));
888		}
889		html
890	}
891
892	pub fn get_bound_field<'a>(&'a self, name: &str) -> Option<BoundField<'a>> {
893		let field = self.get_field(name)?;
894		let data = self.data.get(name);
895		let errors = self.errors.get(name).map(|e| e.as_slice()).unwrap_or(&[]);
896
897		Some(BoundField::new(
898			"form".to_string(),
899			field,
900			data,
901			errors,
902			&self.prefix,
903		))
904	}
905}
906
907impl Default for Form {
908	fn default() -> Self {
909		Self::new()
910	}
911}
912
913/// Safe field access by name.
914///
915/// Returns `None` if the field is not found instead of panicking.
916///
917/// # Examples
918///
919/// ```
920/// use reinhardt_forms::{Form, CharField, Field};
921///
922/// let mut form = Form::new();
923/// form.add_field(Box::new(CharField::new("name".to_string())));
924///
925/// assert!(form.get("name").is_some());
926/// assert!(form.get("nonexistent").is_none());
927/// ```
928impl Form {
929	// Allow borrowed_box because Index trait impl requires &Box<dyn FormField>
930	#[allow(clippy::borrowed_box)]
931	pub fn get(&self, name: &str) -> Option<&Box<dyn FormField>> {
932		self.fields.iter().find(|f| f.name() == name)
933	}
934}
935
936impl Index<&str> for Form {
937	type Output = Box<dyn FormField>;
938
939	fn index(&self, name: &str) -> &Self::Output {
940		self.get(name)
941			.unwrap_or_else(|| panic!("Field '{}' not found", name))
942	}
943}
944
945#[cfg(test)]
946mod tests {
947	use super::*;
948	use crate::fields::CharField;
949
950	#[test]
951	fn test_form_validation() {
952		let mut form = Form::new();
953
954		let mut name_field = CharField::new("name".to_string());
955		name_field.max_length = Some(50);
956		form.add_field(Box::new(name_field));
957
958		let mut data = HashMap::new();
959		data.insert("name".to_string(), serde_json::json!("John Doe"));
960
961		form.bind(data);
962		assert!(form.is_valid());
963		assert!(form.errors().is_empty());
964	}
965
966	#[test]
967	fn test_form_validation_error() {
968		let mut form = Form::new();
969
970		let mut name_field = CharField::new("name".to_string());
971		name_field.max_length = Some(5);
972		form.add_field(Box::new(name_field));
973
974		let mut data = HashMap::new();
975		data.insert("name".to_string(), serde_json::json!("Very Long Name"));
976
977		form.bind(data);
978		assert!(!form.is_valid());
979		assert!(!form.errors().is_empty());
980	}
981
982	// Additional tests based on Django forms tests
983
984	#[test]
985	fn test_form_basic() {
986		// Test based on Django FormsTestCase.test_form
987		use crate::fields::CharField;
988
989		let mut form = Form::new();
990		form.add_field(Box::new(CharField::new("first_name".to_string())));
991		form.add_field(Box::new(CharField::new("last_name".to_string())));
992
993		let mut data = HashMap::new();
994		data.insert("first_name".to_string(), serde_json::json!("John"));
995		data.insert("last_name".to_string(), serde_json::json!("Lennon"));
996
997		form.bind(data);
998
999		assert!(form.is_bound());
1000		assert!(form.is_valid());
1001		assert!(form.errors().is_empty());
1002
1003		// Check cleaned data
1004		let cleaned = form.cleaned_data();
1005		assert_eq!(
1006			cleaned.get("first_name").unwrap(),
1007			&serde_json::json!("John")
1008		);
1009		assert_eq!(
1010			cleaned.get("last_name").unwrap(),
1011			&serde_json::json!("Lennon")
1012		);
1013	}
1014
1015	#[test]
1016	fn test_form_missing_required_fields() {
1017		// Form with missing required fields should have errors
1018		use crate::fields::CharField;
1019
1020		let mut form = Form::new();
1021		form.add_field(Box::new(CharField::new("username".to_string()).required()));
1022		form.add_field(Box::new(CharField::new("email".to_string()).required()));
1023
1024		let data = HashMap::new(); // Empty data
1025
1026		form.bind(data);
1027
1028		assert!(form.is_bound());
1029		assert!(!form.is_valid());
1030		assert!(form.errors().contains_key("username"));
1031		assert!(form.errors().contains_key("email"));
1032	}
1033
1034	#[test]
1035	fn test_form_optional_fields() {
1036		// Form with optional fields should validate even if they're missing
1037		use crate::fields::CharField;
1038
1039		let mut form = Form::new();
1040
1041		let username_field = CharField::new("username".to_string());
1042		form.add_field(Box::new(username_field));
1043
1044		let mut bio_field = CharField::new("bio".to_string());
1045		bio_field.required = false;
1046		form.add_field(Box::new(bio_field));
1047
1048		let mut data = HashMap::new();
1049		data.insert("username".to_string(), serde_json::json!("john"));
1050		// bio is omitted
1051
1052		form.bind(data);
1053
1054		assert!(form.is_bound());
1055		assert!(form.is_valid());
1056		assert!(form.errors().is_empty());
1057	}
1058
1059	#[test]
1060	fn test_form_unbound() {
1061		// Unbound form (no data provided)
1062		use crate::fields::CharField;
1063
1064		let mut form = Form::new();
1065		form.add_field(Box::new(CharField::new("name".to_string())));
1066
1067		assert!(!form.is_bound());
1068		assert!(!form.is_valid()); // Unbound forms are not valid
1069	}
1070
1071	#[test]
1072	fn test_form_extra_data() {
1073		// Form should ignore extra data not defined in fields
1074		use crate::fields::CharField;
1075
1076		let mut form = Form::new();
1077		form.add_field(Box::new(CharField::new("name".to_string())));
1078
1079		let mut data = HashMap::new();
1080		data.insert("name".to_string(), serde_json::json!("John"));
1081		data.insert(
1082			"extra_field".to_string(),
1083			serde_json::json!("should be ignored"),
1084		);
1085
1086		form.bind(data);
1087
1088		assert!(form.is_valid());
1089		let cleaned = form.cleaned_data();
1090		assert_eq!(cleaned.get("name").unwrap(), &serde_json::json!("John"));
1091		// extra_field is still in data but not validated
1092		assert!(cleaned.contains_key("extra_field"));
1093	}
1094
1095	#[test]
1096	fn test_forms_form_multiple_fields() {
1097		// Test form with multiple field types
1098		use crate::fields::{CharField, IntegerField};
1099
1100		let mut form = Form::new();
1101		form.add_field(Box::new(CharField::new("username".to_string())));
1102
1103		let mut age_field = IntegerField::new("age".to_string());
1104		age_field.min_value = Some(0);
1105		age_field.max_value = Some(150);
1106		form.add_field(Box::new(age_field));
1107
1108		let mut data = HashMap::new();
1109		data.insert("username".to_string(), serde_json::json!("alice"));
1110		data.insert("age".to_string(), serde_json::json!(30));
1111
1112		form.bind(data);
1113
1114		assert!(form.is_valid());
1115		assert!(form.errors().is_empty());
1116	}
1117
1118	#[test]
1119	fn test_form_multiple_fields_invalid() {
1120		// Test form with multiple field types, some invalid
1121		use crate::fields::{CharField, IntegerField};
1122
1123		let mut form = Form::new();
1124
1125		let mut username_field = CharField::new("username".to_string());
1126		username_field.min_length = Some(3);
1127		form.add_field(Box::new(username_field));
1128
1129		let mut age_field = IntegerField::new("age".to_string());
1130		age_field.min_value = Some(0);
1131		age_field.max_value = Some(150);
1132		form.add_field(Box::new(age_field));
1133
1134		let mut data = HashMap::new();
1135		data.insert("username".to_string(), serde_json::json!("ab")); // Too short
1136		data.insert("age".to_string(), serde_json::json!(200)); // Too large
1137
1138		form.bind(data);
1139
1140		assert!(!form.is_valid());
1141		assert!(form.errors().contains_key("username"));
1142		assert!(form.errors().contains_key("age"));
1143	}
1144
1145	#[test]
1146	fn test_form_multiple_instances() {
1147		// Multiple form instances should be independent
1148		use crate::fields::CharField;
1149
1150		let mut form1 = Form::new();
1151		form1.add_field(Box::new(CharField::new("name".to_string())));
1152
1153		let mut form2 = Form::new();
1154		form2.add_field(Box::new(CharField::new("name".to_string())));
1155
1156		let mut data1 = HashMap::new();
1157		data1.insert("name".to_string(), serde_json::json!("Form1"));
1158		form1.bind(data1);
1159
1160		let mut data2 = HashMap::new();
1161		data2.insert("name".to_string(), serde_json::json!("Form2"));
1162		form2.bind(data2);
1163
1164		assert!(form1.is_valid());
1165		assert!(form2.is_valid());
1166
1167		assert_eq!(
1168			form1.cleaned_data().get("name").unwrap(),
1169			&serde_json::json!("Form1")
1170		);
1171		assert_eq!(
1172			form2.cleaned_data().get("name").unwrap(),
1173			&serde_json::json!("Form2")
1174		);
1175	}
1176
1177	#[test]
1178	fn test_form_with_initial_data() {
1179		let mut initial = HashMap::new();
1180		initial.insert("name".to_string(), serde_json::json!("Initial Name"));
1181		initial.insert("age".to_string(), serde_json::json!(25));
1182
1183		let mut form = Form::with_initial(initial);
1184
1185		let name_field = CharField::new("name".to_string());
1186		form.add_field(Box::new(name_field));
1187
1188		let age_field = crate::IntegerField::new("age".to_string());
1189		form.add_field(Box::new(age_field));
1190
1191		assert_eq!(
1192			form.initial().get("name").unwrap(),
1193			&serde_json::json!("Initial Name")
1194		);
1195		assert_eq!(form.initial().get("age").unwrap(), &serde_json::json!(25));
1196	}
1197
1198	#[test]
1199	fn test_form_has_changed() {
1200		let mut initial = HashMap::new();
1201		initial.insert("name".to_string(), serde_json::json!("John"));
1202
1203		let mut form = Form::with_initial(initial);
1204
1205		let name_field = CharField::new("name".to_string());
1206		form.add_field(Box::new(name_field));
1207
1208		// Same data as initial - should not have changed
1209		let mut data1 = HashMap::new();
1210		data1.insert("name".to_string(), serde_json::json!("John"));
1211		form.bind(data1);
1212		assert!(!form.has_changed());
1213
1214		// Different data - should have changed
1215		let mut data2 = HashMap::new();
1216		data2.insert("name".to_string(), serde_json::json!("Jane"));
1217		form.bind(data2);
1218		assert!(form.has_changed());
1219	}
1220
1221	#[test]
1222	fn test_form_index_access() {
1223		let mut form = Form::new();
1224
1225		let name_field = CharField::new("name".to_string());
1226		form.add_field(Box::new(name_field));
1227
1228		let field = &form["name"];
1229		assert_eq!(field.name(), "name");
1230	}
1231
1232	#[test]
1233	#[should_panic(expected = "Field 'nonexistent' not found")]
1234	fn test_form_index_access_nonexistent() {
1235		let form = Form::new();
1236		let _ = &form["nonexistent"];
1237	}
1238
1239	#[test]
1240	fn test_form_get_field() {
1241		let mut form = Form::new();
1242
1243		let name_field = CharField::new("name".to_string());
1244		form.add_field(Box::new(name_field));
1245
1246		assert!(form.get_field("name").is_some());
1247		assert!(form.get_field("nonexistent").is_none());
1248	}
1249
1250	#[test]
1251	fn test_form_remove_field() {
1252		let mut form = Form::new();
1253
1254		let name_field = CharField::new("name".to_string());
1255		form.add_field(Box::new(name_field));
1256
1257		assert_eq!(form.field_count(), 1);
1258
1259		let removed = form.remove_field("name");
1260		assert!(removed.is_some());
1261		assert_eq!(form.field_count(), 0);
1262
1263		let not_removed = form.remove_field("nonexistent");
1264		assert!(not_removed.is_none());
1265	}
1266
1267	#[test]
1268	fn test_form_custom_validation() {
1269		let mut form = Form::new();
1270
1271		let mut password_field = CharField::new("password".to_string());
1272		password_field.min_length = Some(8);
1273		form.add_field(Box::new(password_field));
1274
1275		let mut confirm_field = CharField::new("confirm".to_string());
1276		confirm_field.min_length = Some(8);
1277		form.add_field(Box::new(confirm_field));
1278
1279		// Add custom validation to check passwords match
1280		form.add_clean_function(|data| {
1281			let password = data.get("password").and_then(|v| v.as_str());
1282			let confirm = data.get("confirm").and_then(|v| v.as_str());
1283
1284			if password != confirm {
1285				return Err(FormError::Validation("Passwords do not match".to_string()));
1286			}
1287
1288			Ok(())
1289		});
1290
1291		// Test with matching passwords
1292		let mut data1 = HashMap::new();
1293		data1.insert("password".to_string(), serde_json::json!("secret123"));
1294		data1.insert("confirm".to_string(), serde_json::json!("secret123"));
1295		form.bind(data1);
1296		assert!(form.is_valid());
1297
1298		// Test with non-matching passwords
1299		let mut data2 = HashMap::new();
1300		data2.insert("password".to_string(), serde_json::json!("secret123"));
1301		data2.insert("confirm".to_string(), serde_json::json!("different"));
1302		form.bind(data2);
1303		assert!(!form.is_valid());
1304		assert!(form.errors().contains_key(ALL_FIELDS_KEY));
1305	}
1306
1307	#[test]
1308	fn test_form_prefix() {
1309		let mut form = Form::with_prefix("profile".to_string());
1310		assert_eq!(form.prefix(), "profile");
1311		assert_eq!(form.add_prefix_to_field_name("name"), "profile-name");
1312
1313		form.set_prefix("user".to_string());
1314		assert_eq!(form.prefix(), "user");
1315		assert_eq!(form.add_prefix_to_field_name("email"), "user-email");
1316	}
1317
1318	#[test]
1319	fn test_form_field_clean_function() {
1320		let mut form = Form::new();
1321
1322		let mut name_field = CharField::new("name".to_string());
1323		name_field.required = true;
1324		form.add_field(Box::new(name_field));
1325
1326		// Add field-specific clean function to uppercase the name
1327		form.add_field_clean_function("name", |value| {
1328			if let Some(s) = value.as_str() {
1329				Ok(serde_json::json!(s.to_uppercase()))
1330			} else {
1331				Err(FormError::Validation("Expected string".to_string()))
1332			}
1333		});
1334
1335		let mut data = HashMap::new();
1336		data.insert("name".to_string(), serde_json::json!("john doe"));
1337		form.bind(data);
1338
1339		assert!(form.is_valid());
1340		assert_eq!(
1341			form.cleaned_data().get("name").unwrap(),
1342			&serde_json::json!("JOHN DOE")
1343		);
1344	}
1345}