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, HashSet};
5use std::ops::Index;
6
7/// Constant-time comparison to prevent timing attacks on CSRF tokens.
8///
9/// Hashes both inputs with SHA-256 to produce fixed-length digests,
10/// then compares the digests in constant time using `subtle`. This
11/// prevents leaking the length of either input through timing.
12fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
13	use sha2::{Digest, Sha256};
14	use subtle::ConstantTimeEq;
15
16	let hash_a = Sha256::digest(a);
17	let hash_b = Sha256::digest(b);
18	hash_a.ct_eq(&hash_b).into()
19}
20
21/// Error type returned when form-level validation fails.
22#[derive(Debug, thiserror::Error)]
23pub enum FormError {
24	/// A validation error on a specific field.
25	#[error("Field error in {field}: {error}")]
26	Field {
27		/// Name of the field that failed validation.
28		field: String,
29		/// The underlying field error.
30		error: FieldError,
31	},
32	/// A form-level validation error not tied to a specific field.
33	#[error("Validation error: {0}")]
34	Validation(String),
35	/// No model instance is available for a save operation.
36	#[error("No model instance available for save operation")]
37	NoInstance,
38}
39
40/// Result type alias for form-level operations.
41pub type FormResult<T> = Result<T, FormError>;
42
43type CleanFunction =
44	Box<dyn Fn(&HashMap<String, serde_json::Value>) -> FormResult<()> + Send + Sync>;
45type FieldCleanFunction =
46	Box<dyn Fn(&serde_json::Value) -> FormResult<serde_json::Value> + Send + Sync>;
47
48/// Special key for form-level (non-field-specific) errors.
49///
50/// In Django, this is `"__all__"`, but in Rust we use a single underscore
51/// to follow Rust conventions for internal/private identifiers.
52pub const ALL_FIELDS_KEY: &str = "_all";
53
54/// Form data structure (Phase 2-A: Enhanced with client-side validation rules)
55pub struct Form {
56	fields: Vec<Box<dyn FormField>>,
57	data: HashMap<String, serde_json::Value>,
58	cleaned_data: HashMap<String, serde_json::Value>,
59	cleaned_field_names: HashSet<String>,
60	initial: HashMap<String, serde_json::Value>,
61	errors: HashMap<String, Vec<String>>,
62	is_bound: bool,
63	validation_complete: bool,
64	clean_functions: Vec<CleanFunction>,
65	field_clean_functions: HashMap<String, FieldCleanFunction>,
66	prefix: String,
67	/// Client-side validation rules (Phase 2-A)
68	/// These rules are transmitted to the client for UX enhancement.
69	/// Server-side validation is still mandatory for security.
70	validation_rules: Vec<ValidationRule>,
71	/// Expected CSRF token for form validation
72	csrf_token: Option<String>,
73	/// Whether CSRF validation is enabled
74	csrf_enabled: bool,
75}
76
77impl Form {
78	/// Create a new empty form
79	///
80	/// # Examples
81	///
82	/// ```
83	/// use reinhardt_forms::Form;
84	///
85	/// let form = Form::new();
86	/// assert!(!form.is_bound());
87	/// assert!(form.fields().is_empty());
88	/// ```
89	pub fn new() -> Self {
90		Self {
91			fields: vec![],
92			data: HashMap::new(),
93			cleaned_data: HashMap::new(),
94			cleaned_field_names: HashSet::new(),
95			initial: HashMap::new(),
96			errors: HashMap::new(),
97			is_bound: false,
98			validation_complete: false,
99			clean_functions: vec![],
100			field_clean_functions: HashMap::new(),
101			prefix: String::new(),
102			validation_rules: vec![],
103			csrf_token: None,
104			csrf_enabled: false,
105		}
106	}
107	/// Create a new form with initial data
108	///
109	/// # Examples
110	///
111	/// ```
112	/// use reinhardt_forms::Form;
113	/// use std::collections::HashMap;
114	/// use serde_json::json;
115	///
116	/// let mut initial = HashMap::new();
117	/// initial.insert("name".to_string(), json!("John"));
118	///
119	/// let form = Form::with_initial(initial);
120	/// assert_eq!(form.initial().get("name"), Some(&json!("John")));
121	/// ```
122	pub fn with_initial(initial: HashMap<String, serde_json::Value>) -> Self {
123		Self {
124			fields: vec![],
125			data: HashMap::new(),
126			cleaned_data: HashMap::new(),
127			cleaned_field_names: HashSet::new(),
128			initial,
129			errors: HashMap::new(),
130			is_bound: false,
131			validation_complete: false,
132			clean_functions: vec![],
133			field_clean_functions: HashMap::new(),
134			prefix: String::new(),
135			validation_rules: vec![],
136			csrf_token: None,
137			csrf_enabled: false,
138		}
139	}
140	/// Create a new form with a field prefix
141	///
142	/// # Examples
143	///
144	/// ```
145	/// use reinhardt_forms::Form;
146	///
147	/// let form = Form::with_prefix("user".to_string());
148	/// assert_eq!(form.prefix(), "user");
149	/// assert_eq!(form.add_prefix_to_field_name("email"), "user-email");
150	/// ```
151	pub fn with_prefix(prefix: String) -> Self {
152		Self {
153			fields: vec![],
154			data: HashMap::new(),
155			cleaned_data: HashMap::new(),
156			cleaned_field_names: HashSet::new(),
157			initial: HashMap::new(),
158			errors: HashMap::new(),
159			is_bound: false,
160			validation_complete: false,
161			clean_functions: vec![],
162			field_clean_functions: HashMap::new(),
163			prefix,
164			validation_rules: vec![],
165			csrf_token: None,
166			csrf_enabled: false,
167		}
168	}
169	/// Add a field to the form
170	///
171	/// # Examples
172	///
173	/// ```
174	/// use reinhardt_forms::{Form, CharField, Field};
175	///
176	/// let mut form = Form::new();
177	/// let field = CharField::new("username".to_string());
178	/// form.add_field(Box::new(field));
179	/// assert_eq!(form.fields().len(), 1);
180	/// ```
181	pub fn add_field(&mut self, field: Box<dyn FormField>) {
182		self.fields.push(field);
183	}
184	/// Bind form data for validation
185	///
186	/// # Examples
187	///
188	/// ```
189	/// use reinhardt_forms::Form;
190	/// use std::collections::HashMap;
191	/// use serde_json::json;
192	///
193	/// let mut form = Form::new();
194	/// let mut data = HashMap::new();
195	/// data.insert("username".to_string(), json!("john"));
196	///
197	/// form.bind(data);
198	/// assert!(form.is_bound());
199	/// ```
200	pub fn bind(&mut self, data: HashMap<String, serde_json::Value>) {
201		self.cleaned_data = data.clone();
202		self.cleaned_field_names.clear();
203		self.data = data;
204		self.is_bound = true;
205		self.validation_complete = false;
206	}
207	/// Validate the form and return true if all fields are valid
208	///
209	/// # Examples
210	///
211	/// ```
212	/// use reinhardt_forms::{Form, CharField, Field};
213	/// use std::collections::HashMap;
214	/// use serde_json::json;
215	///
216	/// let mut form = Form::new();
217	/// form.add_field(Box::new(CharField::new("username".to_string())));
218	///
219	/// let mut data = HashMap::new();
220	/// data.insert("username".to_string(), json!("john"));
221	/// form.bind(data);
222	///
223	/// assert!(form.is_valid());
224	/// assert!(form.errors().is_empty());
225	/// assert_eq!(form.cleaned_data().get("username"), Some(&json!("john")));
226	/// ```
227	pub fn is_valid(&mut self) -> bool {
228		if !self.is_bound {
229			return false;
230		}
231
232		self.validation_complete = false;
233		self.errors.clear();
234		self.cleaned_data = self.data.clone();
235		self.cleaned_field_names.clear();
236
237		// Validate CSRF token if enabled
238		if !self.validate_csrf() {
239			self.errors
240				.entry(ALL_FIELDS_KEY.to_string())
241				.or_default()
242				.push("CSRF token missing or incorrect.".to_string());
243			return false;
244		}
245
246		for field in &self.fields {
247			let value = self.data_for_field(field.name());
248
249			match field.clean(value) {
250				Ok(mut cleaned) => {
251					// Run field-specific clean function if exists
252					if let Some(field_clean) = self.field_clean_functions.get(field.name()) {
253						match field_clean(&cleaned) {
254							Ok(further_cleaned) => {
255								cleaned = further_cleaned;
256							}
257							Err(e) => {
258								self.errors
259									.entry(field.name().to_string())
260									.or_default()
261									.push(e.to_string());
262								continue;
263							}
264						}
265					}
266					self.cleaned_field_names.insert(field.name().to_string());
267					let submitted_name = self.add_prefix_to_field_name(field.name());
268					if submitted_name != field.name()
269						&& !self.cleaned_field_names.contains(&submitted_name)
270					{
271						self.cleaned_data.remove(&submitted_name);
272					}
273					self.cleaned_data.insert(field.name().to_string(), cleaned);
274				}
275				Err(e) => {
276					self.errors
277						.entry(field.name().to_string())
278						.or_default()
279						.push(e.to_string());
280				}
281			}
282		}
283
284		// Run custom clean functions
285		for clean_fn in &self.clean_functions {
286			if let Err(e) = clean_fn(&self.cleaned_data) {
287				match e {
288					FormError::Field { field, error } => {
289						self.errors
290							.entry(field)
291							.or_default()
292							.push(error.to_string());
293					}
294					FormError::Validation(msg) => {
295						self.errors
296							.entry(ALL_FIELDS_KEY.to_string())
297							.or_default()
298							.push(msg);
299					}
300					FormError::NoInstance => {
301						self.errors
302							.entry(ALL_FIELDS_KEY.to_string())
303							.or_default()
304							.push(e.to_string());
305					}
306				}
307			}
308		}
309
310		self.validation_complete = true;
311		self.errors.is_empty()
312	}
313	/// Returns the cleaned (validated) form data.
314	pub fn cleaned_data(&self) -> &HashMap<String, serde_json::Value> {
315		&self.cleaned_data
316	}
317	/// Returns the current validation errors keyed by field name.
318	pub fn errors(&self) -> &HashMap<String, Vec<String>> {
319		&self.errors
320	}
321	/// Append an error message to the given field's error list.
322	///
323	/// Use [`ALL_FIELDS_KEY`] for non-field (form-wide / cross-field) errors so
324	/// they are exposed through the same inspection API as per-field errors.
325	pub fn add_error(&mut self, field_name: impl Into<String>, message: impl Into<String>) {
326		self.errors
327			.entry(field_name.into())
328			.or_default()
329			.push(message.into());
330	}
331	/// Returns whether the form has been bound with submitted data.
332	pub fn is_bound(&self) -> bool {
333		self.is_bound
334	}
335	/// Returns the list of fields registered on this form.
336	pub fn fields(&self) -> &[Box<dyn FormField>] {
337		&self.fields
338	}
339	/// Returns the initial (default) values for the form.
340	pub fn initial(&self) -> &HashMap<String, serde_json::Value> {
341		&self.initial
342	}
343	/// Set initial data for the form
344	///
345	/// # Examples
346	///
347	/// ```
348	/// use reinhardt_forms::Form;
349	/// use std::collections::HashMap;
350	/// use serde_json::json;
351	///
352	/// let mut form = Form::new();
353	/// let mut initial = HashMap::new();
354	/// initial.insert("name".to_string(), json!("John"));
355	/// form.set_initial(initial);
356	/// ```
357	pub fn set_initial(&mut self, initial: HashMap<String, serde_json::Value>) {
358		self.initial = initial;
359	}
360	/// Check if any field has changed from its initial value
361	///
362	/// # Examples
363	///
364	/// ```
365	/// use reinhardt_forms::{Form, CharField, Field};
366	/// use std::collections::HashMap;
367	/// use serde_json::json;
368	///
369	/// let mut initial = HashMap::new();
370	/// initial.insert("name".to_string(), json!("John"));
371	///
372	/// let mut form = Form::with_initial(initial);
373	/// form.add_field(Box::new(CharField::new("name".to_string())));
374	///
375	/// let mut data = HashMap::new();
376	/// data.insert("name".to_string(), json!("Jane"));
377	/// form.bind(data);
378	///
379	/// assert!(form.has_changed());
380	/// ```
381	pub fn has_changed(&self) -> bool {
382		if !self.is_bound {
383			return false;
384		}
385
386		for field in &self.fields {
387			let initial_val = self.initial.get(field.name());
388			let data_val = if self.validation_complete {
389				self.cleaned_data_for_field(field.name())
390					.or_else(|| self.data_for_field(field.name()))
391			} else {
392				self.data_for_field(field.name())
393			};
394			if field.has_changed(initial_val, data_val) {
395				return true;
396			}
397		}
398		false
399	}
400	/// Looks up a field by name, returning a reference if found.
401	pub fn get_field(&self, name: &str) -> Option<&dyn FormField> {
402		self.fields
403			.iter()
404			.find(|f| f.name() == name)
405			.map(|f| f.as_ref())
406	}
407	/// Removes and returns a field by name, or `None` if not found.
408	pub fn remove_field(&mut self, name: &str) -> Option<Box<dyn FormField>> {
409		let pos = self.fields.iter().position(|f| f.name() == name)?;
410		Some(self.fields.remove(pos))
411	}
412	/// Returns the number of fields registered on this form.
413	pub fn field_count(&self) -> usize {
414		self.fields.len()
415	}
416	/// Add a custom clean function for form validation
417	///
418	/// # Examples
419	///
420	/// ```
421	/// use reinhardt_forms::Form;
422	/// use std::collections::HashMap;
423	/// use serde_json::json;
424	///
425	/// let mut form = Form::new();
426	/// form.add_clean_function(|data| {
427	///     if data.get("password") != data.get("confirm_password") {
428	///         Err(reinhardt_forms::FormError::Validation("Passwords do not match".to_string()))
429	///     } else {
430	///         Ok(())
431	///     }
432	/// });
433	/// ```
434	pub fn add_clean_function<F>(&mut self, f: F)
435	where
436		F: Fn(&HashMap<String, serde_json::Value>) -> FormResult<()> + Send + Sync + 'static,
437	{
438		self.clean_functions.push(Box::new(f));
439	}
440	/// Add a custom clean function for a specific field
441	///
442	/// # Examples
443	///
444	/// ```
445	/// use reinhardt_forms::Form;
446	/// use serde_json::json;
447	///
448	/// let mut form = Form::new();
449	/// form.add_field_clean_function("email", |value| {
450	///     if let Some(email) = value.as_str() {
451	///         if email.contains("@") {
452	///             Ok(value.clone())
453	///         } else {
454	///             Err(reinhardt_forms::FormError::Validation("Invalid email".to_string()))
455	///         }
456	///     } else {
457	///         Ok(value.clone())
458	///     }
459	/// });
460	/// ```
461	pub fn add_field_clean_function<F>(&mut self, field_name: &str, f: F)
462	where
463		F: Fn(&serde_json::Value) -> FormResult<serde_json::Value> + Send + Sync + 'static,
464	{
465		self.field_clean_functions
466			.insert(field_name.to_string(), Box::new(f));
467	}
468
469	/// Get client-side validation rules (Phase 2-A)
470	///
471	/// # Returns
472	///
473	/// Reference to the validation rules vector
474	pub fn validation_rules(&self) -> &[ValidationRule] {
475		&self.validation_rules
476	}
477
478	/// Add a minimum length validator (Phase 2-A)
479	///
480	/// Adds a validator that checks if a string field has at least `min` characters.
481	/// This validator is executed on the client-side for immediate feedback.
482	///
483	/// **Security Note**: Client-side validation is for UX enhancement only.
484	/// Server-side validation is still mandatory for security.
485	///
486	/// # Arguments
487	///
488	/// - `field_name`: Name of the field to validate
489	/// - `min`: Minimum required length
490	/// - `error_message`: Error message to display on validation failure
491	///
492	/// # Examples
493	///
494	/// ```
495	/// use reinhardt_forms::Form;
496	///
497	/// let mut form = Form::new();
498	/// form.add_min_length_validator("password", 8, "Password must be at least 8 characters");
499	/// ```
500	pub fn add_min_length_validator(
501		&mut self,
502		field_name: impl Into<String>,
503		min: usize,
504		error_message: impl Into<String>,
505	) {
506		self.validation_rules.push(ValidationRule::MinLength {
507			field_name: field_name.into(),
508			min,
509			error_message: error_message.into(),
510		});
511	}
512
513	/// Add a maximum length validator (Phase 2-A)
514	///
515	/// Adds a validator that checks if a string field has at most `max` characters.
516	///
517	/// # Examples
518	///
519	/// ```
520	/// use reinhardt_forms::Form;
521	///
522	/// let mut form = Form::new();
523	/// form.add_max_length_validator("username", 50, "Username must be at most 50 characters");
524	/// ```
525	pub fn add_max_length_validator(
526		&mut self,
527		field_name: impl Into<String>,
528		max: usize,
529		error_message: impl Into<String>,
530	) {
531		self.validation_rules.push(ValidationRule::MaxLength {
532			field_name: field_name.into(),
533			max,
534			error_message: error_message.into(),
535		});
536	}
537
538	/// Add a pattern validator (Phase 2-A)
539	///
540	/// Adds a validator that checks if a string field matches a regex pattern.
541	///
542	/// # Examples
543	///
544	/// ```
545	/// use reinhardt_forms::Form;
546	///
547	/// let mut form = Form::new();
548	/// form.add_pattern_validator("code", "^[A-Z]{3}$", "Code must be 3 uppercase letters");
549	/// ```
550	pub fn add_pattern_validator(
551		&mut self,
552		field_name: impl Into<String>,
553		pattern: impl Into<String>,
554		error_message: impl Into<String>,
555	) {
556		self.validation_rules.push(ValidationRule::Pattern {
557			field_name: field_name.into(),
558			pattern: pattern.into(),
559			error_message: error_message.into(),
560		});
561	}
562
563	/// Add a minimum value validator (Phase 2-A)
564	///
565	/// Adds a validator that checks if a numeric field is at least `min`.
566	///
567	/// # Examples
568	///
569	/// ```
570	/// use reinhardt_forms::Form;
571	///
572	/// let mut form = Form::new();
573	/// form.add_min_value_validator("age", 0.0, "Age must be non-negative");
574	/// ```
575	pub fn add_min_value_validator(
576		&mut self,
577		field_name: impl Into<String>,
578		min: f64,
579		error_message: impl Into<String>,
580	) {
581		self.validation_rules.push(ValidationRule::MinValue {
582			field_name: field_name.into(),
583			min,
584			error_message: error_message.into(),
585		});
586	}
587
588	/// Add a maximum value validator (Phase 2-A)
589	///
590	/// Adds a validator that checks if a numeric field is at most `max`.
591	///
592	/// # Examples
593	///
594	/// ```
595	/// use reinhardt_forms::Form;
596	///
597	/// let mut form = Form::new();
598	/// form.add_max_value_validator("age", 150.0, "Age must be at most 150");
599	/// ```
600	pub fn add_max_value_validator(
601		&mut self,
602		field_name: impl Into<String>,
603		max: f64,
604		error_message: impl Into<String>,
605	) {
606		self.validation_rules.push(ValidationRule::MaxValue {
607			field_name: field_name.into(),
608			max,
609			error_message: error_message.into(),
610		});
611	}
612
613	/// Add an email format validator (Phase 2-A)
614	///
615	/// Adds a validator that checks if a field contains a valid email format.
616	///
617	/// # Examples
618	///
619	/// ```
620	/// use reinhardt_forms::Form;
621	///
622	/// let mut form = Form::new();
623	/// form.add_email_validator("email", "Enter a valid email address");
624	/// ```
625	pub fn add_email_validator(
626		&mut self,
627		field_name: impl Into<String>,
628		error_message: impl Into<String>,
629	) {
630		self.validation_rules.push(ValidationRule::Email {
631			field_name: field_name.into(),
632			error_message: error_message.into(),
633		});
634	}
635
636	/// Add a URL format validator (Phase 2-A)
637	///
638	/// Adds a validator that checks if a field contains a valid URL format.
639	///
640	/// # Examples
641	///
642	/// ```
643	/// use reinhardt_forms::Form;
644	///
645	/// let mut form = Form::new();
646	/// form.add_url_validator("website", "Enter a valid URL");
647	/// ```
648	pub fn add_url_validator(
649		&mut self,
650		field_name: impl Into<String>,
651		error_message: impl Into<String>,
652	) {
653		self.validation_rules.push(ValidationRule::Url {
654			field_name: field_name.into(),
655			error_message: error_message.into(),
656		});
657	}
658
659	/// Add a fields equality validator (Phase 2-A)
660	///
661	/// Adds a validator that checks if multiple fields have equal values.
662	/// Commonly used for password confirmation.
663	///
664	/// # Arguments
665	///
666	/// - `field_names`: Names of fields to compare for equality
667	/// - `error_message`: Error message to display on validation failure
668	/// - `target_field`: Target field for error display (None = non-field error)
669	///
670	/// # Examples
671	///
672	/// ```
673	/// use reinhardt_forms::Form;
674	///
675	/// let mut form = Form::new();
676	/// form.add_fields_equal_validator(
677	///     vec!["password".to_string(), "password_confirm".to_string()],
678	///     "Passwords do not match",
679	///     Some("password_confirm".to_string())
680	/// );
681	/// ```
682	pub fn add_fields_equal_validator(
683		&mut self,
684		field_names: Vec<String>,
685		error_message: impl Into<String>,
686		target_field: Option<String>,
687	) {
688		self.validation_rules.push(ValidationRule::FieldsEqual {
689			field_names,
690			error_message: error_message.into(),
691			target_field,
692		});
693	}
694
695	/// Add a client-side validator reference (Phase 2-A)
696	///
697	/// Adds a reference to a reinhardt-validators Validator.
698	/// This validator is executed on the client-side for immediate feedback.
699	///
700	/// **Security Note**: Client-side validation is for UX enhancement only.
701	/// Server-side validation is still mandatory for security.
702	///
703	/// # Arguments
704	///
705	/// - `field_name`: Name of the field to validate
706	/// - `validator_id`: Validator identifier (e.g., "email", "url", "min_length")
707	/// - `params`: Validator parameters as JSON
708	/// - `error_message`: Error message to display on validation failure
709	///
710	/// # Examples
711	///
712	/// ```
713	/// use reinhardt_forms::Form;
714	/// use serde_json::json;
715	///
716	/// let mut form = Form::new();
717	/// form.add_validator_rule(
718	///     "email",
719	///     "email",
720	///     json!({}),
721	///     "Enter a valid email address"
722	/// );
723	///
724	/// form.add_validator_rule(
725	///     "username",
726	///     "min_length",
727	///     json!({"min": 3}),
728	///     "Username must be at least 3 characters"
729	/// );
730	/// ```
731	pub fn add_validator_rule(
732		&mut self,
733		field_name: impl Into<String>,
734		validator_id: impl Into<String>,
735		params: serde_json::Value,
736		error_message: impl Into<String>,
737	) {
738		self.validation_rules.push(ValidationRule::ValidatorRef {
739			field_name: field_name.into(),
740			validator_id: validator_id.into(),
741			params,
742			error_message: error_message.into(),
743		});
744	}
745
746	/// Helper: Add a date range validator (Phase 2-A)
747	///
748	/// Adds a validator that checks if end_date >= start_date.
749	///
750	/// # Arguments
751	///
752	/// - `start_field`: Name of the start date field
753	/// - `end_field`: Name of the end date field
754	/// - `error_message`: Error message (optional, defaults to a standard message)
755	///
756	/// # Examples
757	///
758	/// ```
759	/// use reinhardt_forms::Form;
760	///
761	/// let mut form = Form::new();
762	/// form.add_date_range_validator("start_date", "end_date", None);
763	/// ```
764	pub fn add_date_range_validator(
765		&mut self,
766		start_field: impl Into<String>,
767		end_field: impl Into<String>,
768		error_message: Option<String>,
769	) {
770		let start = start_field.into();
771		let end = end_field.into();
772		let message = error_message
773			.unwrap_or_else(|| "End date must be after or equal to start date".to_string());
774
775		self.validation_rules.push(ValidationRule::DateRange {
776			start_field: start,
777			end_field: end.clone(),
778			error_message: message,
779			target_field: Some(end),
780		});
781	}
782
783	/// Helper: Add a numeric range validator (Phase 2-A)
784	///
785	/// Adds a validator that checks if max >= min.
786	///
787	/// # Arguments
788	///
789	/// - `min_field`: Name of the minimum value field
790	/// - `max_field`: Name of the maximum value field
791	/// - `error_message`: Error message (optional, defaults to a standard message)
792	///
793	/// # Examples
794	///
795	/// ```
796	/// use reinhardt_forms::Form;
797	///
798	/// let mut form = Form::new();
799	/// form.add_numeric_range_validator("min_price", "max_price", None);
800	/// ```
801	pub fn add_numeric_range_validator(
802		&mut self,
803		min_field: impl Into<String>,
804		max_field: impl Into<String>,
805		error_message: Option<String>,
806	) {
807		let min = min_field.into();
808		let max = max_field.into();
809		let message = error_message.unwrap_or_else(|| {
810			"Maximum value must be greater than or equal to minimum value".to_string()
811		});
812
813		self.validation_rules.push(ValidationRule::NumericRange {
814			min_field: min,
815			max_field: max.clone(),
816			error_message: message,
817			target_field: Some(max),
818		});
819	}
820	/// Enable CSRF protection for this form.
821	///
822	/// When enabled, `is_valid()` will check that the submitted data
823	/// contains a matching CSRF token.
824	///
825	/// # Arguments
826	///
827	/// * `token` - The expected CSRF token for this form
828	///
829	/// # Examples
830	///
831	/// ```
832	/// use reinhardt_forms::Form;
833	///
834	/// let mut form = Form::new();
835	/// form.set_csrf_token("abc123".to_string());
836	/// assert!(form.csrf_enabled());
837	/// ```
838	pub fn set_csrf_token(&mut self, token: String) {
839		self.csrf_token = Some(token);
840		self.csrf_enabled = true;
841	}
842
843	/// Check if CSRF protection is enabled
844	pub fn csrf_enabled(&self) -> bool {
845		self.csrf_enabled
846	}
847
848	/// Get the CSRF token, if set
849	pub fn csrf_token(&self) -> Option<&str> {
850		self.csrf_token.as_deref()
851	}
852
853	/// Validate the submitted CSRF token against the expected token.
854	///
855	/// Returns `true` if CSRF is disabled or the token matches.
856	fn validate_csrf(&self) -> bool {
857		if !self.csrf_enabled {
858			return true;
859		}
860
861		let expected = match &self.csrf_token {
862			Some(t) => t,
863			None => return false,
864		};
865
866		let submitted = self
867			.data
868			.get("csrfmiddlewaretoken")
869			.and_then(|v| v.as_str());
870
871		match submitted {
872			Some(token) => {
873				// Use constant-time comparison to prevent timing attacks
874				constant_time_eq(token.as_bytes(), expected.as_bytes())
875			}
876			None => false,
877		}
878	}
879
880	/// Returns the field name prefix for this form.
881	pub fn prefix(&self) -> &str {
882		&self.prefix
883	}
884	/// Sets the field name prefix for this form.
885	pub fn set_prefix(&mut self, prefix: String) {
886		self.prefix = prefix;
887	}
888	/// Returns the field name with the form prefix prepended (e.g., "prefix-field").
889	pub fn add_prefix_to_field_name(&self, field_name: &str) -> String {
890		if self.prefix.is_empty() {
891			field_name.to_string()
892		} else {
893			format!("{}-{}", self.prefix, field_name)
894		}
895	}
896
897	fn data_for_field(&self, field_name: &str) -> Option<&serde_json::Value> {
898		if self.prefix.is_empty() {
899			self.data.get(field_name)
900		} else {
901			let prefixed_name = self.add_prefix_to_field_name(field_name);
902			self.data.get(&prefixed_name)
903		}
904	}
905
906	fn cleaned_data_for_field(&self, field_name: &str) -> Option<&serde_json::Value> {
907		if !self.cleaned_field_names.contains(field_name) {
908			return None;
909		}
910
911		self.cleaned_data.get(field_name)
912	}
913	/// Render CSS `<link>` tags for form media with HTML-escaped paths.
914	///
915	/// All paths are escaped using `escape_attribute()` to prevent XSS
916	/// via malicious CSS file paths.
917	///
918	/// # Arguments
919	///
920	/// * `css_files` - Slice of CSS file paths to include
921	///
922	/// # Examples
923	///
924	/// ```
925	/// use reinhardt_forms::Form;
926	///
927	/// let form = Form::new();
928	/// let html = form.render_css_media(&["/static/forms.css"]);
929	/// assert!(html.contains("href=\"/static/forms.css\""));
930	/// ```
931	pub fn render_css_media(&self, css_files: &[&str]) -> String {
932		use crate::field::escape_attribute;
933		let mut html = String::new();
934		for path in css_files {
935			html.push_str(&format!(
936				"<link rel=\"stylesheet\" href=\"{}\" />\n",
937				escape_attribute(path)
938			));
939		}
940		html
941	}
942
943	/// Render JS `<script>` tags for form media with HTML-escaped paths.
944	///
945	/// All paths are escaped using `escape_attribute()` to prevent XSS
946	/// via malicious JS file paths.
947	///
948	/// # Arguments
949	///
950	/// * `js_files` - Slice of JS file paths to include
951	///
952	/// # Examples
953	///
954	/// ```
955	/// use reinhardt_forms::Form;
956	///
957	/// let form = Form::new();
958	/// let html = form.render_js_media(&["/static/forms.js"]);
959	/// assert!(html.contains("src=\"/static/forms.js\""));
960	/// ```
961	pub fn render_js_media(&self, js_files: &[&str]) -> String {
962		use crate::field::escape_attribute;
963		let mut html = String::new();
964		for path in js_files {
965			html.push_str(&format!(
966				"<script src=\"{}\"></script>\n",
967				escape_attribute(path)
968			));
969		}
970		html
971	}
972
973	/// Returns a `BoundField` with the field's submitted data and errors attached.
974	pub fn get_bound_field<'a>(&'a self, name: &str) -> Option<BoundField<'a>> {
975		let field = self.get_field(name)?;
976		let data = if field.is_sensitive()
977			&& self.validation_complete
978			&& self.cleaned_field_names.contains(field.name())
979		{
980			self.cleaned_data_for_field(name)
981		} else {
982			self.data_for_field(name)
983		};
984		let errors = self.errors.get(name).map(|e| e.as_slice()).unwrap_or(&[]);
985
986		Some(BoundField::new(
987			"form".to_string(),
988			field,
989			data,
990			errors,
991			&self.prefix,
992		))
993	}
994}
995
996impl Default for Form {
997	fn default() -> Self {
998		Self::new()
999	}
1000}
1001
1002/// Safe field access by name.
1003///
1004/// Returns `None` if the field is not found instead of panicking.
1005///
1006/// # Examples
1007///
1008/// ```
1009/// use reinhardt_forms::{Form, CharField, Field};
1010///
1011/// let mut form = Form::new();
1012/// form.add_field(Box::new(CharField::new("name".to_string())));
1013///
1014/// assert!(form.get("name").is_some());
1015/// assert!(form.get("nonexistent").is_none());
1016/// ```
1017impl Form {
1018	// Allow borrowed_box because Index trait impl requires &Box<dyn FormField>
1019	#[allow(clippy::borrowed_box)]
1020	/// Looks up a field by name, returning a reference to the boxed field.
1021	pub fn get(&self, name: &str) -> Option<&Box<dyn FormField>> {
1022		self.fields.iter().find(|f| f.name() == name)
1023	}
1024}
1025
1026impl Index<&str> for Form {
1027	type Output = Box<dyn FormField>;
1028
1029	fn index(&self, name: &str) -> &Self::Output {
1030		self.get(name)
1031			.unwrap_or_else(|| panic!("Field '{}' not found", name))
1032	}
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037	use super::*;
1038	use crate::fields::{CharField, IntegerField};
1039	use rstest::rstest;
1040	use serde_json::json;
1041
1042	#[test]
1043	fn test_form_validation() {
1044		let mut form = Form::new();
1045
1046		let mut name_field = CharField::new("name".to_string());
1047		name_field.max_length = Some(50);
1048		form.add_field(Box::new(name_field));
1049
1050		let mut data = HashMap::new();
1051		data.insert("name".to_string(), serde_json::json!("John Doe"));
1052
1053		form.bind(data);
1054		assert!(form.is_valid());
1055		assert!(form.errors().is_empty());
1056	}
1057
1058	#[test]
1059	fn test_form_validation_error() {
1060		let mut form = Form::new();
1061
1062		let mut name_field = CharField::new("name".to_string());
1063		name_field.max_length = Some(5);
1064		form.add_field(Box::new(name_field));
1065
1066		let mut data = HashMap::new();
1067		data.insert("name".to_string(), serde_json::json!("Very Long Name"));
1068
1069		form.bind(data);
1070		assert!(!form.is_valid());
1071		assert!(!form.errors().is_empty());
1072	}
1073
1074	// Additional tests based on Django forms tests
1075
1076	#[test]
1077	fn test_form_basic() {
1078		// Test based on Django FormsTestCase.test_form
1079		use crate::fields::CharField;
1080
1081		let mut form = Form::new();
1082		form.add_field(Box::new(CharField::new("first_name".to_string())));
1083		form.add_field(Box::new(CharField::new("last_name".to_string())));
1084
1085		let mut data = HashMap::new();
1086		data.insert("first_name".to_string(), serde_json::json!("John"));
1087		data.insert("last_name".to_string(), serde_json::json!("Lennon"));
1088
1089		form.bind(data);
1090
1091		assert!(form.is_bound());
1092		assert!(form.is_valid());
1093		assert!(form.errors().is_empty());
1094
1095		// Check cleaned data
1096		let cleaned = form.cleaned_data();
1097		assert_eq!(
1098			cleaned.get("first_name").unwrap(),
1099			&serde_json::json!("John")
1100		);
1101		assert_eq!(
1102			cleaned.get("last_name").unwrap(),
1103			&serde_json::json!("Lennon")
1104		);
1105	}
1106
1107	#[test]
1108	fn test_form_missing_required_fields() {
1109		// Form with missing required fields should have errors
1110		use crate::fields::CharField;
1111
1112		let mut form = Form::new();
1113		form.add_field(Box::new(CharField::new("username".to_string()).required()));
1114		form.add_field(Box::new(CharField::new("email".to_string()).required()));
1115
1116		let data = HashMap::new(); // Empty data
1117
1118		form.bind(data);
1119
1120		assert!(form.is_bound());
1121		assert!(!form.is_valid());
1122		assert!(form.errors().contains_key("username"));
1123		assert!(form.errors().contains_key("email"));
1124	}
1125
1126	#[test]
1127	fn test_form_optional_fields() {
1128		// Form with optional fields should validate even if they're missing
1129		use crate::fields::CharField;
1130
1131		let mut form = Form::new();
1132
1133		let username_field = CharField::new("username".to_string());
1134		form.add_field(Box::new(username_field));
1135
1136		let mut bio_field = CharField::new("bio".to_string());
1137		bio_field.required = false;
1138		form.add_field(Box::new(bio_field));
1139
1140		let mut data = HashMap::new();
1141		data.insert("username".to_string(), serde_json::json!("john"));
1142		// bio is omitted
1143
1144		form.bind(data);
1145
1146		assert!(form.is_bound());
1147		assert!(form.is_valid());
1148		assert!(form.errors().is_empty());
1149	}
1150
1151	#[test]
1152	fn test_form_unbound() {
1153		// Unbound form (no data provided)
1154		use crate::fields::CharField;
1155
1156		let mut form = Form::new();
1157		form.add_field(Box::new(CharField::new("name".to_string())));
1158
1159		assert!(!form.is_bound());
1160		assert!(!form.is_valid()); // Unbound forms are not valid
1161	}
1162
1163	#[test]
1164	fn test_form_extra_data() {
1165		// Form should ignore extra data not defined in fields
1166		use crate::fields::CharField;
1167
1168		let mut form = Form::new();
1169		form.add_field(Box::new(CharField::new("name".to_string())));
1170
1171		let mut data = HashMap::new();
1172		data.insert("name".to_string(), serde_json::json!("John"));
1173		data.insert(
1174			"extra_field".to_string(),
1175			serde_json::json!("should be ignored"),
1176		);
1177
1178		form.bind(data);
1179
1180		assert!(form.is_valid());
1181		let cleaned = form.cleaned_data();
1182		assert_eq!(cleaned.get("name").unwrap(), &serde_json::json!("John"));
1183		// extra_field is still in data but not validated
1184		assert!(cleaned.contains_key("extra_field"));
1185	}
1186
1187	#[test]
1188	fn test_forms_form_multiple_fields() {
1189		// Test form with multiple field types
1190		use crate::fields::{CharField, IntegerField};
1191
1192		let mut form = Form::new();
1193		form.add_field(Box::new(CharField::new("username".to_string())));
1194
1195		let mut age_field = IntegerField::new("age".to_string());
1196		age_field.min_value = Some(0);
1197		age_field.max_value = Some(150);
1198		form.add_field(Box::new(age_field));
1199
1200		let mut data = HashMap::new();
1201		data.insert("username".to_string(), serde_json::json!("alice"));
1202		data.insert("age".to_string(), serde_json::json!(30));
1203
1204		form.bind(data);
1205
1206		assert!(form.is_valid());
1207		assert!(form.errors().is_empty());
1208	}
1209
1210	#[test]
1211	fn test_form_multiple_fields_invalid() {
1212		// Test form with multiple field types, some invalid
1213		use crate::fields::{CharField, IntegerField};
1214
1215		let mut form = Form::new();
1216
1217		let mut username_field = CharField::new("username".to_string());
1218		username_field.min_length = Some(3);
1219		form.add_field(Box::new(username_field));
1220
1221		let mut age_field = IntegerField::new("age".to_string());
1222		age_field.min_value = Some(0);
1223		age_field.max_value = Some(150);
1224		form.add_field(Box::new(age_field));
1225
1226		let mut data = HashMap::new();
1227		data.insert("username".to_string(), serde_json::json!("ab")); // Too short
1228		data.insert("age".to_string(), serde_json::json!(200)); // Too large
1229
1230		form.bind(data);
1231
1232		assert!(!form.is_valid());
1233		assert!(form.errors().contains_key("username"));
1234		assert!(form.errors().contains_key("age"));
1235	}
1236
1237	#[test]
1238	fn test_form_multiple_instances() {
1239		// Multiple form instances should be independent
1240		use crate::fields::CharField;
1241
1242		let mut form1 = Form::new();
1243		form1.add_field(Box::new(CharField::new("name".to_string())));
1244
1245		let mut form2 = Form::new();
1246		form2.add_field(Box::new(CharField::new("name".to_string())));
1247
1248		let mut data1 = HashMap::new();
1249		data1.insert("name".to_string(), serde_json::json!("Form1"));
1250		form1.bind(data1);
1251
1252		let mut data2 = HashMap::new();
1253		data2.insert("name".to_string(), serde_json::json!("Form2"));
1254		form2.bind(data2);
1255
1256		assert!(form1.is_valid());
1257		assert!(form2.is_valid());
1258
1259		assert_eq!(
1260			form1.cleaned_data().get("name").unwrap(),
1261			&serde_json::json!("Form1")
1262		);
1263		assert_eq!(
1264			form2.cleaned_data().get("name").unwrap(),
1265			&serde_json::json!("Form2")
1266		);
1267	}
1268
1269	#[test]
1270	fn test_form_with_initial_data() {
1271		let mut initial = HashMap::new();
1272		initial.insert("name".to_string(), serde_json::json!("Initial Name"));
1273		initial.insert("age".to_string(), serde_json::json!(25));
1274
1275		let mut form = Form::with_initial(initial);
1276
1277		let name_field = CharField::new("name".to_string());
1278		form.add_field(Box::new(name_field));
1279
1280		let age_field = crate::IntegerField::new("age".to_string());
1281		form.add_field(Box::new(age_field));
1282
1283		assert_eq!(
1284			form.initial().get("name").unwrap(),
1285			&serde_json::json!("Initial Name")
1286		);
1287		assert_eq!(form.initial().get("age").unwrap(), &serde_json::json!(25));
1288	}
1289
1290	#[test]
1291	fn test_form_has_changed() {
1292		let mut initial = HashMap::new();
1293		initial.insert("name".to_string(), serde_json::json!("John"));
1294
1295		let mut form = Form::with_initial(initial);
1296
1297		let name_field = CharField::new("name".to_string());
1298		form.add_field(Box::new(name_field));
1299
1300		// Same data as initial - should not have changed
1301		let mut data1 = HashMap::new();
1302		data1.insert("name".to_string(), serde_json::json!("John"));
1303		form.bind(data1);
1304		assert!(!form.has_changed());
1305
1306		// Different data - should have changed
1307		let mut data2 = HashMap::new();
1308		data2.insert("name".to_string(), serde_json::json!("Jane"));
1309		form.bind(data2);
1310		assert!(form.has_changed());
1311	}
1312
1313	#[test]
1314	fn test_form_index_access() {
1315		let mut form = Form::new();
1316
1317		let name_field = CharField::new("name".to_string());
1318		form.add_field(Box::new(name_field));
1319
1320		let field = &form["name"];
1321		assert_eq!(field.name(), "name");
1322	}
1323
1324	#[test]
1325	#[should_panic(expected = "Field 'nonexistent' not found")]
1326	fn test_form_index_access_nonexistent() {
1327		let form = Form::new();
1328		let _ = &form["nonexistent"];
1329	}
1330
1331	#[test]
1332	fn test_form_get_field() {
1333		let mut form = Form::new();
1334
1335		let name_field = CharField::new("name".to_string());
1336		form.add_field(Box::new(name_field));
1337
1338		assert!(form.get_field("name").is_some());
1339		assert!(form.get_field("nonexistent").is_none());
1340	}
1341
1342	#[test]
1343	fn test_form_remove_field() {
1344		let mut form = Form::new();
1345
1346		let name_field = CharField::new("name".to_string());
1347		form.add_field(Box::new(name_field));
1348
1349		assert_eq!(form.field_count(), 1);
1350
1351		let removed = form.remove_field("name");
1352		assert!(removed.is_some());
1353		assert_eq!(form.field_count(), 0);
1354
1355		let not_removed = form.remove_field("nonexistent");
1356		assert!(not_removed.is_none());
1357	}
1358
1359	#[test]
1360	fn test_form_custom_validation() {
1361		let mut form = Form::new();
1362
1363		let mut password_field = CharField::new("password".to_string());
1364		password_field.min_length = Some(8);
1365		form.add_field(Box::new(password_field));
1366
1367		let mut confirm_field = CharField::new("confirm".to_string());
1368		confirm_field.min_length = Some(8);
1369		form.add_field(Box::new(confirm_field));
1370
1371		// Add custom validation to check passwords match
1372		form.add_clean_function(|data| {
1373			let password = data.get("password").and_then(|v| v.as_str());
1374			let confirm = data.get("confirm").and_then(|v| v.as_str());
1375
1376			if password != confirm {
1377				return Err(FormError::Validation("Passwords do not match".to_string()));
1378			}
1379
1380			Ok(())
1381		});
1382
1383		// Test with matching passwords
1384		let mut data1 = HashMap::new();
1385		data1.insert("password".to_string(), serde_json::json!("secret123"));
1386		data1.insert("confirm".to_string(), serde_json::json!("secret123"));
1387		form.bind(data1);
1388		assert!(form.is_valid());
1389
1390		// Test with non-matching passwords
1391		let mut data2 = HashMap::new();
1392		data2.insert("password".to_string(), serde_json::json!("secret123"));
1393		data2.insert("confirm".to_string(), serde_json::json!("different"));
1394		form.bind(data2);
1395		assert!(!form.is_valid());
1396		assert!(form.errors().contains_key(ALL_FIELDS_KEY));
1397	}
1398
1399	#[rstest]
1400	fn test_form_prefix() {
1401		let mut form = Form::with_prefix("profile".to_string());
1402		assert_eq!(form.prefix(), "profile");
1403		assert_eq!(form.add_prefix_to_field_name("name"), "profile-name");
1404
1405		form.set_prefix("user".to_string());
1406		assert_eq!(form.prefix(), "user");
1407		assert_eq!(form.add_prefix_to_field_name("email"), "user-email");
1408	}
1409
1410	#[rstest]
1411	fn prefixed_forms_do_not_fallback_to_unprefixed_values() {
1412		// Arrange
1413		let mut form = Form::with_prefix("profile".to_string());
1414		form.add_field(Box::new(CharField::new("name".to_string()).required()));
1415		form.bind(HashMap::from([(String::from("name"), json!("other-form"))]));
1416
1417		// Act
1418		let valid = form.is_valid();
1419
1420		// Assert
1421		assert!(!valid);
1422		assert_eq!(form.errors().get("name"), Some(&vec!["name".to_string()]));
1423	}
1424
1425	#[rstest]
1426	fn prefixed_forms_expose_only_canonical_cleaned_values() {
1427		// Arrange
1428		let mut form = Form::with_prefix("profile".to_string());
1429		form.add_field(Box::new(CharField::new("name".to_string()).required()));
1430		form.bind(HashMap::from([(
1431			String::from("profile-name"),
1432			json!("Ada"),
1433		)]));
1434
1435		// Act
1436		let valid = form.is_valid();
1437
1438		// Assert
1439		assert!(valid);
1440		assert_eq!(
1441			form.cleaned_data(),
1442			&HashMap::from([(String::from("name"), json!("Ada"))])
1443		);
1444	}
1445
1446	#[rstest]
1447	fn has_changed_uses_cleaned_values_after_validation() {
1448		// Arrange
1449		let mut form = Form::with_initial(HashMap::from([("age".to_string(), json!(1))]));
1450		form.add_field(Box::new(IntegerField::new("age".to_string())));
1451		form.bind(HashMap::from([("age".to_string(), json!("1"))]));
1452
1453		// Act
1454		let valid = form.is_valid();
1455
1456		// Assert
1457		assert!(valid);
1458		assert_eq!(form.cleaned_data().get("age"), Some(&json!(1)));
1459		assert!(!form.has_changed());
1460	}
1461
1462	#[rstest]
1463	fn has_changed_keeps_cleaned_values_after_later_field_error() {
1464		// Arrange
1465		let mut form = Form::with_initial(HashMap::from([("age".to_string(), json!(1))]));
1466		form.add_field(Box::new(IntegerField::new("age".to_string())));
1467		form.add_clean_function(|_| {
1468			Err(FormError::Field {
1469				field: "age".to_string(),
1470				error: FieldError::validation(None, "Age is not allowed."),
1471			})
1472		});
1473		form.bind(HashMap::from([("age".to_string(), json!("1"))]));
1474
1475		// Act
1476		let valid = form.is_valid();
1477
1478		// Assert
1479		assert!(!valid);
1480		assert_eq!(form.cleaned_data().get("age"), Some(&json!(1)));
1481		assert_eq!(
1482			form.errors().get("age"),
1483			Some(&vec![String::from("Age is not allowed.")])
1484		);
1485		assert!(!form.has_changed());
1486	}
1487
1488	#[rstest]
1489	fn prefixed_forms_preserve_overlapping_canonical_cleaned_values() {
1490		// Arrange
1491		let mut form = Form::with_prefix("profile".to_string());
1492		form.add_field(Box::new(
1493			CharField::new("profile-name".to_string()).required(),
1494		));
1495		form.add_field(Box::new(CharField::new("name".to_string()).required()));
1496		form.bind(HashMap::from([
1497			("profile-profile-name".to_string(), json!("Ada")),
1498			("profile-name".to_string(), json!("Grace")),
1499		]));
1500
1501		// Act
1502		let valid = form.is_valid();
1503
1504		// Assert
1505		assert!(valid);
1506		assert_eq!(form.cleaned_data().get("profile-name"), Some(&json!("Ada")));
1507		assert_eq!(form.cleaned_data().get("name"), Some(&json!("Grace")));
1508	}
1509
1510	#[rstest]
1511	fn prefixed_forms_preserve_bound_values_after_validation_failure() {
1512		// Arrange
1513		let mut form = Form::with_prefix("profile".to_string());
1514		form.add_field(Box::new(CharField::new("name".to_string()).required()));
1515		form.add_field(Box::new(CharField::new("email".to_string()).required()));
1516		let expected_name = json!("Ada");
1517		form.bind(HashMap::from([(
1518			String::from("profile-name"),
1519			expected_name.clone(),
1520		)]));
1521
1522		// Act
1523		let first_valid = form.is_valid();
1524		let first_bound_value = form.get_bound_field("name").unwrap().value().cloned();
1525		let second_valid = form.is_valid();
1526		let second_bound_value = form.get_bound_field("name").unwrap().value().cloned();
1527
1528		// Assert
1529		assert!(!first_valid);
1530		assert!(!second_valid);
1531		assert_eq!(first_bound_value, Some(expected_name.clone()));
1532		assert_eq!(second_bound_value, Some(expected_name));
1533	}
1534
1535	#[test]
1536	fn test_form_field_clean_function() {
1537		let mut form = Form::new();
1538
1539		let mut name_field = CharField::new("name".to_string());
1540		name_field.required = true;
1541		form.add_field(Box::new(name_field));
1542
1543		// Add field-specific clean function to uppercase the name
1544		form.add_field_clean_function("name", |value| {
1545			if let Some(s) = value.as_str() {
1546				Ok(serde_json::json!(s.to_uppercase()))
1547			} else {
1548				Err(FormError::Validation("Expected string".to_string()))
1549			}
1550		});
1551
1552		let mut data = HashMap::new();
1553		data.insert("name".to_string(), serde_json::json!("john doe"));
1554		form.bind(data);
1555
1556		assert!(form.is_valid());
1557		assert_eq!(
1558			form.cleaned_data().get("name").unwrap(),
1559			&serde_json::json!("JOHN DOE")
1560		);
1561	}
1562
1563	#[rstest]
1564	fn form_configuration_and_submission_edges_are_observable() {
1565		// Arrange
1566		let mut form = Form::with_prefix("profile".to_string());
1567		form.add_min_length_validator("username", 3, "Username is too short.");
1568		form.add_max_length_validator("username", 24, "Username is too long.");
1569		form.add_pattern_validator("username", "^[a-z]+$", "Lowercase letters only.");
1570		form.add_min_value_validator("age", 18.0, "Adults only.");
1571		form.add_max_value_validator("age", 120.0, "Age is too large.");
1572		form.add_email_validator("email", "Enter a valid email.");
1573		form.add_url_validator("website", "Enter a valid URL.");
1574		form.add_fields_equal_validator(
1575			vec!["password".to_string(), "confirmation".to_string()],
1576			"Passwords do not match.",
1577			Some("confirmation".to_string()),
1578		);
1579		form.add_validator_rule(
1580			"username",
1581			"reserved_words",
1582			json!({"scope": "registration"}),
1583			"Username is reserved.",
1584		);
1585		form.add_date_range_validator("starts_at", "ends_at", None);
1586		form.add_numeric_range_validator("minimum", "maximum", None);
1587
1588		// Act and assert
1589		assert_eq!(
1590			serde_json::to_value(form.validation_rules()).unwrap(),
1591			json!([
1592				{
1593					"type": "min_length",
1594					"field_name": "username",
1595					"min": 3,
1596					"error_message": "Username is too short."
1597				},
1598				{
1599					"type": "max_length",
1600					"field_name": "username",
1601					"max": 24,
1602					"error_message": "Username is too long."
1603				},
1604				{
1605					"type": "pattern",
1606					"field_name": "username",
1607					"pattern": "^[a-z]+$",
1608					"error_message": "Lowercase letters only."
1609				},
1610				{
1611					"type": "min_value",
1612					"field_name": "age",
1613					"min": 18.0,
1614					"error_message": "Adults only."
1615				},
1616				{
1617					"type": "max_value",
1618					"field_name": "age",
1619					"max": 120.0,
1620					"error_message": "Age is too large."
1621				},
1622				{
1623					"type": "email",
1624					"field_name": "email",
1625					"error_message": "Enter a valid email."
1626				},
1627				{
1628					"type": "url",
1629					"field_name": "website",
1630					"error_message": "Enter a valid URL."
1631				},
1632				{
1633					"type": "fields_equal",
1634					"field_names": ["password", "confirmation"],
1635					"error_message": "Passwords do not match.",
1636					"target_field": "confirmation"
1637				},
1638				{
1639					"type": "validator_ref",
1640					"field_name": "username",
1641					"validator_id": "reserved_words",
1642					"params": {"scope": "registration"},
1643					"error_message": "Username is reserved."
1644				},
1645				{
1646					"type": "date_range",
1647					"start_field": "starts_at",
1648					"end_field": "ends_at",
1649					"error_message": "End date must be after or equal to start date",
1650					"target_field": "ends_at"
1651				},
1652				{
1653					"type": "numeric_range",
1654					"min_field": "minimum",
1655					"max_field": "maximum",
1656					"error_message": "Maximum value must be greater than or equal to minimum value",
1657					"target_field": "maximum"
1658				}
1659			]),
1660		);
1661		assert_eq!(form.prefix(), "profile");
1662		assert_eq!(form.add_prefix_to_field_name("email"), "profile-email");
1663		form.set_prefix("account".to_string());
1664		assert_eq!(form.add_prefix_to_field_name("email"), "account-email");
1665		assert_eq!(
1666			form.render_css_media(&["/assets/<theme>&.css"]),
1667			"<link rel=\"stylesheet\" href=\"/assets/&lt;theme&gt;&amp;.css\" />\n",
1668		);
1669		assert_eq!(
1670			form.render_js_media(&["/assets/\"main\".js"]),
1671			"<script src=\"/assets/&quot;main&quot;.js\"></script>\n",
1672		);
1673
1674		let mut csrf_form = Form::new();
1675		csrf_form.set_csrf_token("expected-token".to_string());
1676		csrf_form.bind(HashMap::new());
1677		assert!(!csrf_form.is_valid());
1678		assert_eq!(
1679			csrf_form.errors().get(ALL_FIELDS_KEY),
1680			Some(&vec!["CSRF token missing or incorrect.".to_string()]),
1681		);
1682		csrf_form.bind(HashMap::from([(
1683			"csrfmiddlewaretoken".to_string(),
1684			json!("wrong-token"),
1685		)]));
1686		assert!(!csrf_form.is_valid());
1687		assert_eq!(
1688			csrf_form.errors().get(ALL_FIELDS_KEY),
1689			Some(&vec!["CSRF token missing or incorrect.".to_string()]),
1690		);
1691		csrf_form.bind(HashMap::from([(
1692			"csrfmiddlewaretoken".to_string(),
1693			json!("expected-token"),
1694		)]));
1695		assert!(csrf_form.is_valid());
1696		csrf_form.add_error(ALL_FIELDS_KEY, "Cross-field validation failed.");
1697		assert_eq!(
1698			csrf_form.errors().get(ALL_FIELDS_KEY),
1699			Some(&vec!["Cross-field validation failed.".to_string()]),
1700		);
1701	}
1702}