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