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