Skip to main content

reinhardt_forms/fields/
advanced_fields.rs

1//! Advanced field types for specialized data validation
2
3use crate::Widget;
4use crate::field::{FieldError, FieldResult, FormField};
5use serde_json::Value;
6use std::collections::HashMap;
7
8/// A field for UUID validation
9///
10/// Validates that the input is a valid UUID (Universally Unique Identifier).
11/// Supports UUID v4 by default.
12///
13/// # Examples
14///
15/// ```
16/// use reinhardt_forms::fields::UUIDField;
17/// use reinhardt_forms::Field;
18/// use serde_json::json;
19///
20/// let field = UUIDField::new("id");
21///
22/// // Valid UUID v4
23/// let result = field.clean(Some(&json!("550e8400-e29b-41d4-a716-446655440000")));
24/// assert!(result.is_ok());
25///
26/// // Invalid UUID
27/// let result = field.clean(Some(&json!("not-a-uuid")));
28/// assert!(result.is_err());
29/// ```
30#[derive(Debug, Clone)]
31pub struct UUIDField {
32	/// The field name used as the form data key.
33	pub name: String,
34	/// Whether this field must be filled in.
35	pub required: bool,
36	/// Custom error messages keyed by error type.
37	pub error_messages: HashMap<String, String>,
38	/// The widget type used for rendering this field.
39	pub widget: Widget,
40	/// Help text displayed alongside the field.
41	pub help_text: String,
42	/// Optional initial (default) value for the field.
43	pub initial: Option<Value>,
44}
45
46impl UUIDField {
47	/// Create a new UUIDField
48	pub fn new(name: impl Into<String>) -> Self {
49		let mut error_messages = HashMap::new();
50		error_messages.insert(
51			"required".to_string(),
52			"This field is required.".to_string(),
53		);
54		error_messages.insert("invalid".to_string(), "Enter a valid UUID.".to_string());
55
56		Self {
57			name: name.into(),
58			required: true,
59			error_messages,
60			widget: Widget::TextInput,
61			help_text: String::new(),
62			initial: None,
63		}
64	}
65
66	/// Set whether this field is required
67	pub fn required(mut self, required: bool) -> Self {
68		self.required = required;
69		self
70	}
71
72	/// Set the help text
73	pub fn help_text(mut self, text: impl Into<String>) -> Self {
74		self.help_text = text.into();
75		self
76	}
77
78	/// Set the initial value
79	pub fn initial(mut self, value: Value) -> Self {
80		self.initial = Some(value);
81		self
82	}
83
84	/// Set a custom error message
85	pub fn error_message(
86		mut self,
87		error_type: impl Into<String>,
88		message: impl Into<String>,
89	) -> Self {
90		self.error_messages
91			.insert(error_type.into(), message.into());
92		self
93	}
94
95	/// Validate UUID format
96	fn validate_uuid(&self, s: &str) -> bool {
97		// UUID format: 8-4-4-4-12 hexadecimal digits
98		let parts: Vec<&str> = s.split('-').collect();
99		if parts.len() != 5 {
100			return false;
101		}
102
103		if parts[0].len() != 8
104			|| parts[1].len() != 4
105			|| parts[2].len() != 4
106			|| parts[3].len() != 4
107			|| parts[4].len() != 12
108		{
109			return false;
110		}
111
112		parts
113			.iter()
114			.all(|part| part.chars().all(|c| c.is_ascii_hexdigit()))
115	}
116}
117
118impl FormField for UUIDField {
119	fn name(&self) -> &str {
120		&self.name
121	}
122
123	fn label(&self) -> Option<&str> {
124		None
125	}
126
127	fn widget(&self) -> &Widget {
128		&self.widget
129	}
130
131	fn required(&self) -> bool {
132		self.required
133	}
134
135	fn initial(&self) -> Option<&Value> {
136		self.initial.as_ref()
137	}
138
139	fn help_text(&self) -> Option<&str> {
140		if self.help_text.is_empty() {
141			None
142		} else {
143			Some(&self.help_text)
144		}
145	}
146
147	fn clean(&self, value: Option<&Value>) -> FieldResult<Value> {
148		if value.is_none() || value == Some(&Value::Null) {
149			if self.required {
150				let error_msg = self
151					.error_messages
152					.get("required")
153					.cloned()
154					.unwrap_or_else(|| "This field is required.".to_string());
155				return Err(FieldError::validation(None, &error_msg));
156			}
157			return Ok(Value::Null);
158		}
159
160		let s = match value.unwrap() {
161			Value::String(s) => s.trim(),
162			_ => {
163				let error_msg = self
164					.error_messages
165					.get("invalid")
166					.cloned()
167					.unwrap_or_else(|| "Enter a valid UUID.".to_string());
168				return Err(FieldError::validation(None, &error_msg));
169			}
170		};
171
172		if s.is_empty() {
173			if self.required {
174				let error_msg = self
175					.error_messages
176					.get("required")
177					.cloned()
178					.unwrap_or_else(|| "This field is required.".to_string());
179				return Err(FieldError::validation(None, &error_msg));
180			}
181			return Ok(Value::Null);
182		}
183
184		if !self.validate_uuid(s) {
185			let error_msg = self
186				.error_messages
187				.get("invalid")
188				.cloned()
189				.unwrap_or_else(|| "Enter a valid UUID.".to_string());
190			return Err(FieldError::validation(None, &error_msg));
191		}
192
193		Ok(Value::String(s.to_lowercase()))
194	}
195
196	fn has_changed(&self, initial: Option<&Value>, data: Option<&Value>) -> bool {
197		match (initial, data) {
198			(None, None) => false,
199			(Some(_), None) | (None, Some(_)) => true,
200			(Some(Value::String(a)), Some(Value::String(b))) => {
201				a.to_lowercase() != b.to_lowercase()
202			}
203			(Some(a), Some(b)) => a != b,
204		}
205	}
206}
207
208/// A field for ISO 8601 duration validation
209///
210/// Validates that the input is a valid ISO 8601 duration format (e.g., "P1Y2M3DT4H5M6S").
211///
212/// # Examples
213///
214/// ```
215/// use reinhardt_forms::fields::DurationField;
216/// use reinhardt_forms::Field;
217/// use serde_json::json;
218///
219/// let field = DurationField::new("duration");
220///
221/// // Valid duration
222/// let result = field.clean(Some(&json!("P1Y2M3DT4H5M6S")));
223/// assert!(result.is_ok());
224///
225/// // Another valid duration (1 day)
226/// let result = field.clean(Some(&json!("P1D")));
227/// assert!(result.is_ok());
228/// ```
229#[derive(Debug, Clone)]
230pub struct DurationField {
231	/// The field name used as the form data key.
232	pub name: String,
233	/// Whether this field must be filled in.
234	pub required: bool,
235	/// Custom error messages keyed by error type.
236	pub error_messages: HashMap<String, String>,
237	/// The widget type used for rendering this field.
238	pub widget: Widget,
239	/// Help text displayed alongside the field.
240	pub help_text: String,
241	/// Optional initial (default) value for the field.
242	pub initial: Option<Value>,
243}
244
245impl DurationField {
246	/// Create a new DurationField
247	pub fn new(name: impl Into<String>) -> Self {
248		let mut error_messages = HashMap::new();
249		error_messages.insert(
250			"required".to_string(),
251			"This field is required.".to_string(),
252		);
253		error_messages.insert(
254			"invalid".to_string(),
255			"Enter a valid ISO 8601 duration.".to_string(),
256		);
257
258		Self {
259			name: name.into(),
260			required: true,
261			error_messages,
262			widget: Widget::TextInput,
263			help_text: String::new(),
264			initial: None,
265		}
266	}
267
268	/// Set whether this field is required
269	pub fn required(mut self, required: bool) -> Self {
270		self.required = required;
271		self
272	}
273
274	/// Set the help text
275	pub fn help_text(mut self, text: impl Into<String>) -> Self {
276		self.help_text = text.into();
277		self
278	}
279
280	/// Set the initial value
281	pub fn initial(mut self, value: Value) -> Self {
282		self.initial = Some(value);
283		self
284	}
285
286	/// Set a custom error message
287	pub fn error_message(
288		mut self,
289		error_type: impl Into<String>,
290		message: impl Into<String>,
291	) -> Self {
292		self.error_messages
293			.insert(error_type.into(), message.into());
294		self
295	}
296
297	/// Validate ISO 8601 duration format
298	/// Format: P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W
299	fn validate_duration(&self, s: &str) -> bool {
300		if !s.starts_with('P') {
301			return false;
302		}
303
304		let s = &s[1..]; // Remove 'P' prefix
305
306		if s.is_empty() {
307			return false;
308		}
309
310		// Week format: P[n]W
311		if let Some(num_part) = s.strip_suffix('W') {
312			return num_part.chars().all(|c| c.is_ascii_digit());
313		}
314
315		// Date and time format
316		let parts: Vec<&str> = s.split('T').collect();
317
318		if parts.is_empty() || parts.len() > 2 {
319			return false;
320		}
321
322		// Validate date part (Y, M, D)
323		let date_valid = self.validate_duration_part(parts[0], &['Y', 'M', 'D']);
324
325		// Validate time part if present (H, M, S)
326		let time_valid = if parts.len() == 2 {
327			!parts[1].is_empty() && self.validate_duration_part(parts[1], &['H', 'M', 'S'])
328		} else {
329			true
330		};
331
332		date_valid && time_valid
333	}
334
335	/// Validate a duration part (either date or time)
336	fn validate_duration_part(&self, part: &str, units: &[char]) -> bool {
337		if part.is_empty() {
338			return true; // Empty parts are okay
339		}
340
341		let mut current_num = String::new();
342
343		for ch in part.chars() {
344			if ch.is_ascii_digit() || ch == '.' {
345				current_num.push(ch);
346			} else if units.contains(&ch) {
347				if current_num.is_empty() {
348					return false;
349				}
350				current_num.clear();
351			} else {
352				return false;
353			}
354		}
355
356		current_num.is_empty() // Should have consumed all digits
357	}
358}
359
360impl FormField for DurationField {
361	fn name(&self) -> &str {
362		&self.name
363	}
364
365	fn label(&self) -> Option<&str> {
366		None
367	}
368
369	fn widget(&self) -> &Widget {
370		&self.widget
371	}
372
373	fn required(&self) -> bool {
374		self.required
375	}
376
377	fn initial(&self) -> Option<&Value> {
378		self.initial.as_ref()
379	}
380
381	fn help_text(&self) -> Option<&str> {
382		if self.help_text.is_empty() {
383			None
384		} else {
385			Some(&self.help_text)
386		}
387	}
388
389	fn clean(&self, value: Option<&Value>) -> FieldResult<Value> {
390		if value.is_none() || value == Some(&Value::Null) {
391			if self.required {
392				let error_msg = self
393					.error_messages
394					.get("required")
395					.cloned()
396					.unwrap_or_else(|| "This field is required.".to_string());
397				return Err(FieldError::validation(None, &error_msg));
398			}
399			return Ok(Value::Null);
400		}
401
402		let s = match value.unwrap() {
403			Value::String(s) => s.trim(),
404			_ => {
405				let error_msg = self
406					.error_messages
407					.get("invalid")
408					.cloned()
409					.unwrap_or_else(|| "Enter a valid ISO 8601 duration.".to_string());
410				return Err(FieldError::validation(None, &error_msg));
411			}
412		};
413
414		if s.is_empty() {
415			if self.required {
416				let error_msg = self
417					.error_messages
418					.get("required")
419					.cloned()
420					.unwrap_or_else(|| "This field is required.".to_string());
421				return Err(FieldError::validation(None, &error_msg));
422			}
423			return Ok(Value::Null);
424		}
425
426		if !self.validate_duration(s) {
427			let error_msg = self
428				.error_messages
429				.get("invalid")
430				.cloned()
431				.unwrap_or_else(|| "Enter a valid ISO 8601 duration.".to_string());
432			return Err(FieldError::validation(None, &error_msg));
433		}
434
435		Ok(Value::String(s.to_uppercase()))
436	}
437
438	fn has_changed(&self, initial: Option<&Value>, data: Option<&Value>) -> bool {
439		match (initial, data) {
440			(None, None) => false,
441			(Some(_), None) | (None, Some(_)) => true,
442			(Some(Value::String(a)), Some(Value::String(b))) => {
443				a.to_uppercase() != b.to_uppercase()
444			}
445			(Some(a), Some(b)) => a != b,
446		}
447	}
448}
449
450/// A field that combines multiple field validators
451///
452/// ComboField runs all provided validators in sequence and requires all to pass.
453///
454/// # Examples
455///
456/// ```
457/// use reinhardt_forms::fields::ComboField;
458/// use reinhardt_forms::{Field, CharField, EmailField};
459/// use serde_json::json;
460///
461/// // Create validators with constraints
462/// let mut email_field = EmailField::new("email".to_string());
463/// let mut char_field = CharField::new("email".to_string());
464/// char_field.min_length = Some(5);
465/// char_field.max_length = Some(100);
466///
467/// // Combine email validation with length validation
468/// let field = ComboField::new("email")
469///     .add_validator(Box::new(email_field))
470///     .add_validator(Box::new(char_field));
471///
472/// // Valid: passes both email and length checks
473/// let result = field.clean(Some(&json!("user@example.com")));
474/// assert!(result.is_ok());
475///
476/// // Invalid: fails email validation
477/// let result = field.clean(Some(&json!("not-an-email")));
478/// assert!(result.is_err());
479///
480/// // Invalid: too short (less than 5 characters)
481/// let result = field.clean(Some(&json!("a@b")));
482/// assert!(result.is_err());
483/// ```
484pub struct ComboField {
485	/// The field name used as the form data key.
486	pub name: String,
487	/// Whether this field must be filled in.
488	pub required: bool,
489	/// Custom error messages keyed by error type.
490	pub error_messages: HashMap<String, String>,
491	/// The widget type used for rendering this field.
492	pub widget: Widget,
493	/// Help text displayed alongside the field.
494	pub help_text: String,
495	/// Optional initial (default) value for the field.
496	pub initial: Option<Value>,
497	/// The list of validator fields that all must pass.
498	pub validators: Vec<Box<dyn FormField>>,
499}
500
501impl ComboField {
502	/// Create a new ComboField
503	pub fn new(name: impl Into<String>) -> Self {
504		let mut error_messages = HashMap::new();
505		error_messages.insert(
506			"required".to_string(),
507			"This field is required.".to_string(),
508		);
509
510		Self {
511			name: name.into(),
512			required: true,
513			error_messages,
514			widget: Widget::TextInput,
515			help_text: String::new(),
516			initial: None,
517			validators: Vec::new(),
518		}
519	}
520
521	/// Add a validator field
522	pub fn add_validator(mut self, validator: Box<dyn FormField>) -> Self {
523		self.validators.push(validator);
524		self
525	}
526
527	/// Set whether this field is required
528	pub fn required(mut self, required: bool) -> Self {
529		self.required = required;
530		self
531	}
532
533	/// Set the help text
534	pub fn help_text(mut self, text: impl Into<String>) -> Self {
535		self.help_text = text.into();
536		self
537	}
538
539	/// Set the initial value
540	pub fn initial(mut self, value: Value) -> Self {
541		self.initial = Some(value);
542		self
543	}
544
545	/// Set a custom error message
546	pub fn error_message(
547		mut self,
548		error_type: impl Into<String>,
549		message: impl Into<String>,
550	) -> Self {
551		self.error_messages
552			.insert(error_type.into(), message.into());
553		self
554	}
555}
556
557impl FormField for ComboField {
558	fn name(&self) -> &str {
559		&self.name
560	}
561
562	fn label(&self) -> Option<&str> {
563		None
564	}
565
566	fn widget(&self) -> &Widget {
567		&self.widget
568	}
569
570	fn required(&self) -> bool {
571		self.required
572	}
573
574	fn initial(&self) -> Option<&Value> {
575		self.initial.as_ref()
576	}
577
578	fn help_text(&self) -> Option<&str> {
579		if self.help_text.is_empty() {
580			None
581		} else {
582			Some(&self.help_text)
583		}
584	}
585
586	fn clean(&self, value: Option<&Value>) -> FieldResult<Value> {
587		if value.is_none() || value == Some(&Value::Null) {
588			if self.required {
589				let error_msg = self
590					.error_messages
591					.get("required")
592					.cloned()
593					.unwrap_or_else(|| "This field is required.".to_string());
594				return Err(FieldError::validation(None, &error_msg));
595			}
596			return Ok(Value::Null);
597		}
598
599		// Run all validators
600		let mut result = value.unwrap().clone();
601		for validator in &self.validators {
602			result = validator.clean(Some(&result))?;
603		}
604
605		Ok(result)
606	}
607
608	fn has_changed(&self, initial: Option<&Value>, data: Option<&Value>) -> bool {
609		match (initial, data) {
610			(None, None) => false,
611			(Some(_), None) | (None, Some(_)) => true,
612			(Some(a), Some(b)) => a != b,
613		}
614	}
615}
616
617/// A field for color validation (hex format)
618///
619/// Validates that the input is a valid hex color code (e.g., "#FF0000" or "#F00").
620///
621/// # Examples
622///
623/// ```
624/// use reinhardt_forms::fields::ColorField;
625/// use reinhardt_forms::Field;
626/// use serde_json::json;
627///
628/// let field = ColorField::new("color");
629///
630/// // Valid 6-digit hex color
631/// let result = field.clean(Some(&json!("#FF0000")));
632/// assert!(result.is_ok());
633///
634/// // Valid 3-digit hex color
635/// let result = field.clean(Some(&json!("#F00")));
636/// assert!(result.is_ok());
637/// ```
638#[derive(Debug, Clone)]
639pub struct ColorField {
640	/// The field name used as the form data key.
641	pub name: String,
642	/// Whether this field must be filled in.
643	pub required: bool,
644	/// Custom error messages keyed by error type.
645	pub error_messages: HashMap<String, String>,
646	/// The widget type used for rendering this field.
647	pub widget: Widget,
648	/// Help text displayed alongside the field.
649	pub help_text: String,
650	/// Optional initial (default) value for the field.
651	pub initial: Option<Value>,
652}
653
654impl ColorField {
655	/// Create a new ColorField
656	pub fn new(name: impl Into<String>) -> Self {
657		let mut error_messages = HashMap::new();
658		error_messages.insert(
659			"required".to_string(),
660			"This field is required.".to_string(),
661		);
662		error_messages.insert(
663			"invalid".to_string(),
664			"Enter a valid hex color code (e.g., #FF0000).".to_string(),
665		);
666
667		Self {
668			name: name.into(),
669			required: true,
670			error_messages,
671			widget: Widget::TextInput,
672			help_text: String::new(),
673			initial: None,
674		}
675	}
676
677	/// Set whether this field is required
678	pub fn required(mut self, required: bool) -> Self {
679		self.required = required;
680		self
681	}
682
683	/// Set the help text
684	pub fn help_text(mut self, text: impl Into<String>) -> Self {
685		self.help_text = text.into();
686		self
687	}
688
689	/// Set the initial value
690	pub fn initial(mut self, value: Value) -> Self {
691		self.initial = Some(value);
692		self
693	}
694
695	/// Validate hex color format
696	fn validate_color(&self, s: &str) -> bool {
697		if !s.starts_with('#') {
698			return false;
699		}
700
701		let hex = &s[1..];
702		if hex.len() != 3 && hex.len() != 6 {
703			return false;
704		}
705
706		hex.chars().all(|c| c.is_ascii_hexdigit())
707	}
708}
709
710impl FormField for ColorField {
711	fn name(&self) -> &str {
712		&self.name
713	}
714
715	fn label(&self) -> Option<&str> {
716		None
717	}
718
719	fn widget(&self) -> &Widget {
720		&self.widget
721	}
722
723	fn required(&self) -> bool {
724		self.required
725	}
726
727	fn initial(&self) -> Option<&Value> {
728		self.initial.as_ref()
729	}
730
731	fn help_text(&self) -> Option<&str> {
732		if self.help_text.is_empty() {
733			None
734		} else {
735			Some(&self.help_text)
736		}
737	}
738
739	fn clean(&self, value: Option<&Value>) -> FieldResult<Value> {
740		if value.is_none() || value == Some(&Value::Null) {
741			if self.required {
742				let error_msg = self
743					.error_messages
744					.get("required")
745					.cloned()
746					.unwrap_or_else(|| "This field is required.".to_string());
747				return Err(FieldError::validation(None, &error_msg));
748			}
749			return Ok(Value::Null);
750		}
751
752		let s = match value.unwrap() {
753			Value::String(s) => s.trim(),
754			_ => {
755				let error_msg = self
756					.error_messages
757					.get("invalid")
758					.cloned()
759					.unwrap_or_else(|| "Enter a valid hex color code.".to_string());
760				return Err(FieldError::validation(None, &error_msg));
761			}
762		};
763
764		if s.is_empty() {
765			if self.required {
766				let error_msg = self
767					.error_messages
768					.get("required")
769					.cloned()
770					.unwrap_or_else(|| "This field is required.".to_string());
771				return Err(FieldError::validation(None, &error_msg));
772			}
773			return Ok(Value::Null);
774		}
775
776		if !self.validate_color(s) {
777			let error_msg = self
778				.error_messages
779				.get("invalid")
780				.cloned()
781				.unwrap_or_else(|| "Enter a valid hex color code.".to_string());
782			return Err(FieldError::validation(None, &error_msg));
783		}
784
785		Ok(Value::String(s.to_uppercase()))
786	}
787
788	fn has_changed(&self, initial: Option<&Value>, data: Option<&Value>) -> bool {
789		match (initial, data) {
790			(None, None) => false,
791			(Some(_), None) | (None, Some(_)) => true,
792			(Some(Value::String(a)), Some(Value::String(b))) => {
793				a.to_uppercase() != b.to_uppercase()
794			}
795			(Some(a), Some(b)) => a != b,
796		}
797	}
798}
799
800/// Redacted placeholder value stored in cleaned data for password fields.
801///
802/// Password fields store this constant instead of the plaintext password
803/// to prevent accidental exposure of credentials in cleaned form data.
804pub const PASSWORD_REDACTED: &str = "**********";
805
806/// A field for password validation with strength requirements
807///
808/// Validates password strength including minimum length, required character types.
809///
810/// # Examples
811///
812/// ```
813/// use reinhardt_forms::fields::PasswordField;
814/// use reinhardt_forms::Field;
815/// use serde_json::json;
816///
817/// let field = PasswordField::new("password")
818///     .min_length(8)
819///     .require_uppercase(true)
820///     .require_digit(true);
821///
822/// // Valid strong password
823/// let result = field.clean(Some(&json!("SecurePass123")));
824/// assert!(result.is_ok());
825///
826/// // Invalid: too short
827/// let result = field.clean(Some(&json!("Pass1")));
828/// assert!(result.is_err());
829/// ```
830#[derive(Debug, Clone)]
831pub struct PasswordField {
832	/// The field name used as the form data key.
833	pub name: String,
834	/// Whether this field must be filled in.
835	pub required: bool,
836	/// Custom error messages keyed by error type.
837	pub error_messages: HashMap<String, String>,
838	/// The widget type used for rendering this field.
839	pub widget: Widget,
840	/// Help text displayed alongside the field.
841	pub help_text: String,
842	/// Optional initial (default) value for the field.
843	pub initial: Option<Value>,
844	/// Minimum required password length.
845	pub min_length: usize,
846	/// Whether the password must contain at least one uppercase letter.
847	pub require_uppercase: bool,
848	/// Whether the password must contain at least one lowercase letter.
849	pub require_lowercase: bool,
850	/// Whether the password must contain at least one digit.
851	pub require_digit: bool,
852	/// Whether the password must contain at least one special character.
853	pub require_special: bool,
854}
855
856impl PasswordField {
857	/// Create a new PasswordField
858	pub fn new(name: impl Into<String>) -> Self {
859		let mut error_messages = HashMap::new();
860		error_messages.insert(
861			"required".to_string(),
862			"This field is required.".to_string(),
863		);
864		error_messages.insert(
865			"too_short".to_string(),
866			"Password must be at least {min_length} characters.".to_string(),
867		);
868		error_messages.insert(
869			"no_uppercase".to_string(),
870			"Password must contain at least one uppercase letter.".to_string(),
871		);
872		error_messages.insert(
873			"no_lowercase".to_string(),
874			"Password must contain at least one lowercase letter.".to_string(),
875		);
876		error_messages.insert(
877			"no_digit".to_string(),
878			"Password must contain at least one digit.".to_string(),
879		);
880		error_messages.insert(
881			"no_special".to_string(),
882			"Password must contain at least one special character.".to_string(),
883		);
884
885		Self {
886			name: name.into(),
887			required: true,
888			error_messages,
889			widget: Widget::PasswordInput,
890			help_text: String::new(),
891			initial: None,
892			min_length: 8,
893			require_uppercase: false,
894			require_lowercase: false,
895			require_digit: false,
896			require_special: false,
897		}
898	}
899
900	/// Set minimum length
901	pub fn min_length(mut self, length: usize) -> Self {
902		self.min_length = length;
903		self
904	}
905
906	/// Require uppercase letter
907	pub fn require_uppercase(mut self, required: bool) -> Self {
908		self.require_uppercase = required;
909		self
910	}
911
912	/// Require lowercase letter
913	pub fn require_lowercase(mut self, required: bool) -> Self {
914		self.require_lowercase = required;
915		self
916	}
917
918	/// Require digit
919	pub fn require_digit(mut self, required: bool) -> Self {
920		self.require_digit = required;
921		self
922	}
923
924	/// Require special character
925	pub fn require_special(mut self, required: bool) -> Self {
926		self.require_special = required;
927		self
928	}
929
930	/// Set whether this field is required
931	pub fn required(mut self, required: bool) -> Self {
932		self.required = required;
933		self
934	}
935
936	/// Set the help text
937	pub fn help_text(mut self, text: impl Into<String>) -> Self {
938		self.help_text = text.into();
939		self
940	}
941}
942
943impl FormField for PasswordField {
944	fn name(&self) -> &str {
945		&self.name
946	}
947
948	fn label(&self) -> Option<&str> {
949		None
950	}
951
952	fn widget(&self) -> &Widget {
953		&self.widget
954	}
955
956	fn is_sensitive(&self) -> bool {
957		true
958	}
959
960	fn required(&self) -> bool {
961		self.required
962	}
963
964	fn initial(&self) -> Option<&Value> {
965		self.initial.as_ref()
966	}
967
968	fn help_text(&self) -> Option<&str> {
969		if self.help_text.is_empty() {
970			None
971		} else {
972			Some(&self.help_text)
973		}
974	}
975
976	fn clean(&self, value: Option<&Value>) -> FieldResult<Value> {
977		if value.is_none() || value == Some(&Value::Null) {
978			if self.required {
979				let error_msg = self
980					.error_messages
981					.get("required")
982					.cloned()
983					.unwrap_or_else(|| "This field is required.".to_string());
984				return Err(FieldError::validation(None, &error_msg));
985			}
986			return Ok(Value::Null);
987		}
988
989		let s = match value.unwrap() {
990			Value::String(s) => s,
991			_ => {
992				return Err(FieldError::validation(None, "Invalid password format."));
993			}
994		};
995
996		if s.is_empty() {
997			if self.required {
998				let error_msg = self
999					.error_messages
1000					.get("required")
1001					.cloned()
1002					.unwrap_or_else(|| "This field is required.".to_string());
1003				return Err(FieldError::validation(None, &error_msg));
1004			}
1005			return Ok(Value::Null);
1006		}
1007
1008		// Check minimum length
1009		if s.len() < self.min_length {
1010			let error_msg = self
1011				.error_messages
1012				.get("too_short")
1013				.cloned()
1014				.unwrap_or_else(|| {
1015					format!("Password must be at least {} characters.", self.min_length)
1016				});
1017			return Err(FieldError::validation(None, &error_msg));
1018		}
1019
1020		// Check uppercase requirement
1021		if self.require_uppercase && !s.chars().any(|c| c.is_uppercase()) {
1022			let error_msg = self
1023				.error_messages
1024				.get("no_uppercase")
1025				.cloned()
1026				.unwrap_or_else(|| {
1027					"Password must contain at least one uppercase letter.".to_string()
1028				});
1029			return Err(FieldError::validation(None, &error_msg));
1030		}
1031
1032		// Check lowercase requirement
1033		if self.require_lowercase && !s.chars().any(|c| c.is_lowercase()) {
1034			let error_msg = self
1035				.error_messages
1036				.get("no_lowercase")
1037				.cloned()
1038				.unwrap_or_else(|| {
1039					"Password must contain at least one lowercase letter.".to_string()
1040				});
1041			return Err(FieldError::validation(None, &error_msg));
1042		}
1043
1044		// Check digit requirement
1045		if self.require_digit && !s.chars().any(|c| c.is_ascii_digit()) {
1046			let error_msg = self
1047				.error_messages
1048				.get("no_digit")
1049				.cloned()
1050				.unwrap_or_else(|| "Password must contain at least one digit.".to_string());
1051			return Err(FieldError::validation(None, &error_msg));
1052		}
1053
1054		// Check special character requirement
1055		if self.require_special && !s.chars().any(|c| !c.is_alphanumeric()) {
1056			let error_msg = self
1057				.error_messages
1058				.get("no_special")
1059				.cloned()
1060				.unwrap_or_else(|| {
1061					"Password must contain at least one special character.".to_string()
1062				});
1063			return Err(FieldError::validation(None, &error_msg));
1064		}
1065
1066		// Redact password to prevent plaintext storage in cleaned data
1067		Ok(Value::String(PASSWORD_REDACTED.to_string()))
1068	}
1069
1070	fn has_changed(&self, initial: Option<&Value>, data: Option<&Value>) -> bool {
1071		match (initial, data) {
1072			(None, None) => false,
1073			(Some(_), None) | (None, Some(_)) => true,
1074			(Some(a), Some(b)) => a != b,
1075		}
1076	}
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081	use super::*;
1082	use rstest::rstest;
1083	use serde_json::json;
1084
1085	#[test]
1086	fn test_uuid_field_valid() {
1087		let field = UUIDField::new("id");
1088
1089		let result = field.clean(Some(&json!("550e8400-e29b-41d4-a716-446655440000")));
1090		assert!(result.is_ok());
1091
1092		// Case insensitive
1093		let result = field.clean(Some(&json!("550E8400-E29B-41D4-A716-446655440000")));
1094		assert!(result.is_ok());
1095	}
1096
1097	#[test]
1098	fn test_uuid_field_invalid() {
1099		let field = UUIDField::new("id");
1100
1101		// Too short
1102		let result = field.clean(Some(&json!("550e8400-e29b")));
1103		assert!(result.is_err());
1104
1105		// Invalid characters
1106		let result = field.clean(Some(&json!("550e8400-e29b-41d4-a716-44665544000g")));
1107		assert!(result.is_err());
1108
1109		// Wrong format
1110		let result = field.clean(Some(&json!("not-a-uuid")));
1111		assert!(result.is_err());
1112	}
1113
1114	#[test]
1115	fn test_duration_field_valid() {
1116		let field = DurationField::new("duration");
1117
1118		// Full format
1119		let result = field.clean(Some(&json!("P1Y2M3DT4H5M6S")));
1120		assert!(result.is_ok());
1121
1122		// Days only
1123		let result = field.clean(Some(&json!("P1D")));
1124		assert!(result.is_ok());
1125
1126		// Time only
1127		let result = field.clean(Some(&json!("PT1H")));
1128		assert!(result.is_ok());
1129
1130		// Weeks
1131		let result = field.clean(Some(&json!("P2W")));
1132		assert!(result.is_ok());
1133	}
1134
1135	#[test]
1136	fn test_duration_field_invalid() {
1137		let field = DurationField::new("duration");
1138
1139		// Missing P prefix
1140		let result = field.clean(Some(&json!("1Y2M")));
1141		assert!(result.is_err());
1142
1143		// Empty after P
1144		let result = field.clean(Some(&json!("P")));
1145		assert!(result.is_err());
1146
1147		// Invalid format
1148		let result = field.clean(Some(&json!("P1X")));
1149		assert!(result.is_err());
1150	}
1151
1152	#[test]
1153	fn test_combo_field() {
1154		use crate::EmailField;
1155
1156		// Create a validator with length and email constraints
1157		let mut char_field_min = crate::CharField::new("text".to_string());
1158		char_field_min.min_length = Some(5);
1159
1160		let mut char_field_max = crate::CharField::new("text".to_string());
1161		char_field_max.max_length = Some(50);
1162
1163		let field = ComboField::new("email")
1164			.add_validator(Box::new(char_field_min))
1165			.add_validator(Box::new(char_field_max))
1166			.add_validator(Box::new(EmailField::new("email".to_string())));
1167
1168		// Valid
1169		let result = field.clean(Some(&json!("test@example.com")));
1170		assert!(result.is_ok());
1171
1172		// Too short
1173		let result = field.clean(Some(&json!("a@b")));
1174		assert!(result.is_err());
1175
1176		// Not an email
1177		let result = field.clean(Some(&json!("hello world")));
1178		assert!(result.is_err());
1179	}
1180
1181	#[test]
1182	fn test_color_field_valid() {
1183		let field = ColorField::new("color");
1184
1185		// 6-digit hex
1186		let result = field.clean(Some(&json!("#FF0000")));
1187		assert!(result.is_ok());
1188
1189		// 3-digit hex
1190		let result = field.clean(Some(&json!("#F00")));
1191		assert!(result.is_ok());
1192
1193		// Lowercase
1194		let result = field.clean(Some(&json!("#ff0000")));
1195		assert!(result.is_ok());
1196	}
1197
1198	#[test]
1199	fn test_color_field_invalid() {
1200		let field = ColorField::new("color");
1201
1202		// Missing #
1203		let result = field.clean(Some(&json!("FF0000")));
1204		assert!(result.is_err());
1205
1206		// Invalid length
1207		let result = field.clean(Some(&json!("#FF00")));
1208		assert!(result.is_err());
1209
1210		// Invalid characters
1211		let result = field.clean(Some(&json!("#GGGGGG")));
1212		assert!(result.is_err());
1213	}
1214
1215	#[test]
1216	fn test_password_field_basic() {
1217		let field = PasswordField::new("password").min_length(6);
1218
1219		// Valid
1220		let result = field.clean(Some(&json!("password123")));
1221		assert!(result.is_ok());
1222
1223		// Too short
1224		let result = field.clean(Some(&json!("pass")));
1225		assert!(result.is_err());
1226	}
1227
1228	#[test]
1229	fn test_password_field_requirements() {
1230		let field = PasswordField::new("password")
1231			.min_length(8)
1232			.require_uppercase(true)
1233			.require_digit(true)
1234			.require_special(true);
1235
1236		// Valid: has uppercase, digit, and special char
1237		let result = field.clean(Some(&json!("SecurePass123!")));
1238		assert!(result.is_ok());
1239
1240		// Missing uppercase
1241		let result = field.clean(Some(&json!("password123!")));
1242		assert!(result.is_err());
1243
1244		// Missing digit
1245		let result = field.clean(Some(&json!("SecurePassword!")));
1246		assert!(result.is_err());
1247
1248		// Missing special char
1249		let result = field.clean(Some(&json!("SecurePassword123")));
1250		assert!(result.is_err());
1251	}
1252
1253	#[rstest]
1254	fn uuid_field_builder_exposes_configured_metadata() {
1255		// Arrange
1256		let uuid = UUIDField::new("identifier")
1257			.required(false)
1258			.help_text("Canonical resource identifier")
1259			.initial(json!("550e8400-e29b-41d4-a716-446655440000"))
1260			.error_message("invalid", "UUID only.");
1261
1262		// Act
1263		let metadata = (
1264			uuid.name(),
1265			uuid.required,
1266			Some(uuid.help_text.as_str()),
1267			uuid.initial.as_ref(),
1268			uuid.error_messages.get("invalid").map(String::as_str),
1269		);
1270
1271		// Assert
1272		assert_eq!(
1273			metadata,
1274			(
1275				"identifier",
1276				false,
1277				Some("Canonical resource identifier"),
1278				Some(&json!("550e8400-e29b-41d4-a716-446655440000")),
1279				Some("UUID only."),
1280			),
1281		);
1282	}
1283
1284	#[rstest]
1285	fn duration_field_builder_exposes_configured_metadata() {
1286		// Arrange
1287		let duration = DurationField::new("timeout")
1288			.required(false)
1289			.help_text("ISO 8601 duration")
1290			.initial(json!("PT30S"))
1291			.error_message("invalid", "Duration only.");
1292
1293		// Act
1294		let metadata = (
1295			duration.name(),
1296			duration.required,
1297			Some(duration.help_text.as_str()),
1298			duration.initial.as_ref(),
1299			duration.error_messages.get("invalid").map(String::as_str),
1300		);
1301
1302		// Assert
1303		assert_eq!(
1304			metadata,
1305			(
1306				"timeout",
1307				false,
1308				Some("ISO 8601 duration"),
1309				Some(&json!("PT30S")),
1310				Some("Duration only."),
1311			),
1312		);
1313	}
1314
1315	#[rstest]
1316	fn combo_field_builder_exposes_configured_metadata() {
1317		// Arrange
1318		let combo = ComboField::new("identifier")
1319			.required(false)
1320			.help_text("Validated identifier")
1321			.initial(json!("initial"))
1322			.error_message("required", "Identifier is optional.")
1323			.add_validator(Box::new(UUIDField::new("identifier")));
1324
1325		// Act
1326		let metadata = (
1327			combo.name(),
1328			combo.required,
1329			Some(combo.help_text.as_str()),
1330			combo.initial.as_ref(),
1331			combo.error_messages.get("required").map(String::as_str),
1332			combo.validators.len(),
1333		);
1334
1335		// Assert
1336		assert_eq!(
1337			metadata,
1338			(
1339				"identifier",
1340				false,
1341				Some("Validated identifier"),
1342				Some(&json!("initial")),
1343				Some("Identifier is optional."),
1344				1,
1345			),
1346		);
1347	}
1348
1349	#[rstest]
1350	fn uuid_field_covers_optional_invalid_type_and_change_boundaries() {
1351		// Arrange
1352		let uuid = UUIDField::new("identifier")
1353			.required(false)
1354			.error_message("invalid", "UUID only.");
1355
1356		// Act and assert
1357		assert_eq!(
1358			uuid.clean(Some(&json!(" 550E8400-E29B-41D4-A716-446655440000 ")))
1359				.unwrap(),
1360			json!("550e8400-e29b-41d4-a716-446655440000"),
1361		);
1362		assert_eq!(uuid.clean(Some(&json!(""))).unwrap(), Value::Null);
1363		assert_eq!(
1364			uuid.clean(Some(&json!(7))).unwrap_err().to_string(),
1365			"UUID only.",
1366		);
1367		assert!(!uuid.has_changed(
1368			Some(&json!("550E8400-E29B-41D4-A716-446655440000")),
1369			Some(&json!("550e8400-e29b-41d4-a716-446655440000")),
1370		));
1371		assert!(uuid.has_changed(
1372			Some(&json!("550e8400-e29b-41d4-a716-446655440000")),
1373			Some(&json!("550e8400-e29b-41d4-a716-446655440001")),
1374		));
1375	}
1376
1377	#[rstest]
1378	fn duration_field_covers_optional_invalid_type_and_change_boundaries() {
1379		// Arrange
1380		let duration = DurationField::new("duration")
1381			.required(false)
1382			.error_message("invalid", "Duration only.");
1383
1384		// Act and assert
1385		assert_eq!(duration.clean(Some(&json!("P0W"))).unwrap(), json!("P0W"));
1386		assert_eq!(
1387			duration.clean(Some(&json!("P999W"))).unwrap(),
1388			json!("P999W")
1389		);
1390		assert_eq!(
1391			duration.clean(Some(&json!(" PT1H30M "))).unwrap(),
1392			json!("PT1H30M"),
1393		);
1394		assert_eq!(duration.clean(Some(&json!(""))).unwrap(), Value::Null);
1395		assert_eq!(
1396			duration.clean(Some(&json!(false))).unwrap_err().to_string(),
1397			"Duration only.",
1398		);
1399		assert_eq!(
1400			duration
1401				.clean(Some(&json!("not-a-duration")))
1402				.unwrap_err()
1403				.to_string(),
1404			"Duration only.",
1405		);
1406		assert!(!duration.has_changed(Some(&json!("PT1H")), Some(&json!("PT1H"))));
1407		assert!(duration.has_changed(Some(&json!("PT1H")), Some(&json!("PT2H"))));
1408	}
1409
1410	#[rstest]
1411	fn color_field_covers_optional_invalid_type_and_change_boundaries() {
1412		// Arrange
1413		let color = ColorField::new("color").required(false);
1414
1415		// Act and assert
1416		assert_eq!(
1417			color.clean(Some(&json!(" #aBc123 "))).unwrap(),
1418			json!("#ABC123")
1419		);
1420		assert_eq!(color.clean(Some(&json!(""))).unwrap(), Value::Null);
1421		assert_eq!(
1422			color.clean(Some(&json!(true))).unwrap_err().to_string(),
1423			"Enter a valid hex color code (e.g., #FF0000).",
1424		);
1425		assert_eq!(
1426			color.clean(Some(&json!("#12G"))).unwrap_err().to_string(),
1427			"Enter a valid hex color code (e.g., #FF0000).",
1428		);
1429		assert!(!color.has_changed(Some(&json!("#abc123")), Some(&json!("#ABC123"))));
1430	}
1431
1432	#[rstest]
1433	fn password_field_covers_optional_invalid_type_and_change_boundaries() {
1434		// Arrange
1435		let password = PasswordField::new("password")
1436			.required(false)
1437			.min_length(8)
1438			.require_uppercase(true)
1439			.require_lowercase(true)
1440			.require_digit(true)
1441			.require_special(true);
1442
1443		// Act and assert
1444		assert_eq!(
1445			password.clean(Some(&json!("Valid1!a"))).unwrap(),
1446			json!(PASSWORD_REDACTED),
1447		);
1448		assert_eq!(
1449			password
1450				.clean(Some(&json!("valid1!a")))
1451				.unwrap_err()
1452				.to_string(),
1453			"Password must contain at least one uppercase letter.",
1454		);
1455		assert_eq!(
1456			password
1457				.clean(Some(&json!("VALID1!A")))
1458				.unwrap_err()
1459				.to_string(),
1460			"Password must contain at least one lowercase letter.",
1461		);
1462		assert_eq!(
1463			password
1464				.clean(Some(&json!("Valid!aa")))
1465				.unwrap_err()
1466				.to_string(),
1467			"Password must contain at least one digit.",
1468		);
1469		assert_eq!(
1470			password
1471				.clean(Some(&json!("Valid1aa")))
1472				.unwrap_err()
1473				.to_string(),
1474			"Password must contain at least one special character.",
1475		);
1476		assert_eq!(
1477			password.clean(Some(&json!({}))).unwrap_err().to_string(),
1478			"Invalid password format.",
1479		);
1480		assert_eq!(password.clean(Some(&json!(""))).unwrap(), Value::Null);
1481	}
1482}