Skip to main content

reinhardt_forms/fields/
email_field.rs

1//! Email field with validation
2
3use crate::field::{FieldError, FieldResult, FormField, Widget};
4use regex::Regex;
5use std::sync::LazyLock;
6
7/// Email validation regex pattern.
8const EMAIL_PATTERN: &str = r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$";
9
10/// Cached email validation regex to avoid repeated compilation.
11static EMAIL_REGEX: LazyLock<Regex> =
12	LazyLock::new(|| Regex::new(EMAIL_PATTERN).expect("Email regex pattern is valid"));
13
14/// Email field with format validation
15#[derive(Debug, Clone)]
16pub struct EmailField {
17	/// The field name used as the form data key.
18	pub name: String,
19	/// Optional human-readable label for display.
20	pub label: Option<String>,
21	/// Whether this field must be filled in.
22	pub required: bool,
23	/// Optional help text displayed alongside the field.
24	pub help_text: Option<String>,
25	/// The widget type used for rendering this field.
26	pub widget: Widget,
27	/// Optional initial (default) value for the field.
28	pub initial: Option<serde_json::Value>,
29	/// Maximum allowed character count (defaults to 320 per RFC).
30	pub max_length: Option<usize>,
31	/// Minimum required character count.
32	pub min_length: Option<usize>,
33}
34
35impl EmailField {
36	/// Create a new EmailField with the given name
37	///
38	/// # Examples
39	///
40	/// ```
41	/// use reinhardt_forms::fields::EmailField;
42	///
43	/// let field = EmailField::new("email".to_string());
44	/// assert_eq!(field.name, "email");
45	/// assert!(!field.required);
46	/// assert_eq!(field.max_length, Some(320));
47	/// ```
48	pub fn new(name: String) -> Self {
49		Self {
50			name,
51			label: None,
52			required: false,
53			help_text: None,
54			widget: Widget::EmailInput,
55			initial: None,
56			max_length: Some(320), // RFC standard: 64 (local) + @ + 255 (domain)
57			min_length: None,
58		}
59	}
60
61	/// Set the field as required
62	///
63	/// # Examples
64	///
65	/// ```
66	/// use reinhardt_forms::fields::EmailField;
67	///
68	/// let field = EmailField::new("contact".to_string()).required();
69	/// assert!(field.required);
70	/// ```
71	pub fn required(mut self) -> Self {
72		self.required = true;
73		self
74	}
75
76	/// Set the maximum length for the field
77	///
78	/// # Examples
79	///
80	/// ```
81	/// use reinhardt_forms::fields::EmailField;
82	///
83	/// let field = EmailField::new("email".to_string()).with_max_length(100);
84	/// assert_eq!(field.max_length, Some(100));
85	/// ```
86	pub fn with_max_length(mut self, max_length: usize) -> Self {
87		self.max_length = Some(max_length);
88		self
89	}
90
91	/// Set the minimum length for the field
92	///
93	/// # Examples
94	///
95	/// ```
96	/// use reinhardt_forms::fields::EmailField;
97	///
98	/// let field = EmailField::new("email".to_string()).with_min_length(5);
99	/// assert_eq!(field.min_length, Some(5));
100	/// ```
101	pub fn with_min_length(mut self, min_length: usize) -> Self {
102		self.min_length = Some(min_length);
103		self
104	}
105
106	/// Set the label for the field
107	///
108	/// # Examples
109	///
110	/// ```
111	/// use reinhardt_forms::fields::EmailField;
112	///
113	/// let field = EmailField::new("contact_email".to_string()).with_label("Email Address");
114	/// assert_eq!(field.label, Some("Email Address".to_string()));
115	/// ```
116	pub fn with_label(mut self, label: impl Into<String>) -> Self {
117		self.label = Some(label.into());
118		self
119	}
120
121	/// Set the help text for the field
122	///
123	/// # Examples
124	///
125	/// ```
126	/// use reinhardt_forms::fields::EmailField;
127	///
128	/// let field = EmailField::new("email".to_string()).with_help_text("We'll never share your email");
129	/// assert_eq!(field.help_text, Some("We'll never share your email".to_string()));
130	/// ```
131	pub fn with_help_text(mut self, help_text: impl Into<String>) -> Self {
132		self.help_text = Some(help_text.into());
133		self
134	}
135
136	/// Set the initial value for the field
137	///
138	/// # Examples
139	///
140	/// ```
141	/// use reinhardt_forms::fields::EmailField;
142	///
143	/// let field = EmailField::new("email".to_string()).with_initial("user@example.com");
144	/// assert_eq!(field.initial, Some(serde_json::json!("user@example.com")));
145	/// ```
146	pub fn with_initial(mut self, initial: impl Into<String>) -> Self {
147		self.initial = Some(serde_json::json!(initial.into()));
148		self
149	}
150
151	/// Validate email format
152	fn validate_email(email: &str) -> bool {
153		EMAIL_REGEX.is_match(email)
154	}
155}
156
157// Note: Default trait is not implemented because EmailField requires a name
158
159impl FormField for EmailField {
160	fn name(&self) -> &str {
161		&self.name
162	}
163
164	fn label(&self) -> Option<&str> {
165		self.label.as_deref()
166	}
167
168	fn required(&self) -> bool {
169		self.required
170	}
171
172	fn help_text(&self) -> Option<&str> {
173		self.help_text.as_deref()
174	}
175
176	fn widget(&self) -> &Widget {
177		&self.widget
178	}
179
180	fn initial(&self) -> Option<&serde_json::Value> {
181		self.initial.as_ref()
182	}
183
184	fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
185		match value {
186			None if self.required => Err(FieldError::Required(self.name.clone())),
187			None => Ok(serde_json::Value::String(String::new())),
188			Some(v) => {
189				let s = v
190					.as_str()
191					.ok_or_else(|| FieldError::Validation("Expected string".to_string()))?;
192
193				// Trim whitespace
194				let s = s.trim();
195
196				// Return empty string if not required and empty
197				if s.is_empty() {
198					if self.required {
199						return Err(FieldError::Required(self.name.clone()));
200					}
201					return Ok(serde_json::Value::String(String::new()));
202				}
203
204				// Check length constraints using character count (not byte count)
205				// for correct multi-byte character handling
206				let char_count = s.chars().count();
207				if let Some(max) = self.max_length
208					&& char_count > max
209				{
210					return Err(FieldError::Validation(format!(
211						"Ensure this value has at most {} characters (it has {})",
212						max, char_count
213					)));
214				}
215
216				if let Some(min) = self.min_length
217					&& char_count < min
218				{
219					return Err(FieldError::Validation(format!(
220						"Ensure this value has at least {} characters (it has {})",
221						min, char_count
222					)));
223				}
224
225				// Validate email format
226				if !Self::validate_email(s) {
227					return Err(FieldError::Validation(
228						"Enter a valid email address".to_string(),
229					));
230				}
231
232				Ok(serde_json::Value::String(s.to_string()))
233			}
234		}
235	}
236}