Skip to main content

reinhardt_forms/fields/
url_field.rs

1use crate::field::{FieldError, FieldResult, FormField, Widget};
2
3/// URLField for URL input
4pub struct URLField {
5	pub name: String,
6	pub label: Option<String>,
7	pub required: bool,
8	pub help_text: Option<String>,
9	pub widget: Widget,
10	pub initial: Option<serde_json::Value>,
11	pub max_length: Option<usize>,
12}
13
14impl URLField {
15	/// Create a new URLField
16	///
17	/// # Examples
18	///
19	/// ```
20	/// use reinhardt_forms::fields::URLField;
21	///
22	/// let field = URLField::new("website".to_string());
23	/// assert_eq!(field.name, "website");
24	/// ```
25	pub fn new(name: String) -> Self {
26		Self {
27			name,
28			label: None,
29			required: true,
30			help_text: None,
31			widget: Widget::TextInput,
32			initial: None,
33			max_length: Some(200),
34		}
35	}
36
37	fn validate_url(url: &str) -> bool {
38		// Basic URL validation
39		let url_regex = regex::Regex::new(
40            r"^https?://(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d+)?(?:/[^\s]*)?$"
41        ).unwrap();
42
43		url_regex.is_match(url)
44	}
45}
46
47impl FormField for URLField {
48	fn name(&self) -> &str {
49		&self.name
50	}
51
52	fn label(&self) -> Option<&str> {
53		self.label.as_deref()
54	}
55
56	fn required(&self) -> bool {
57		self.required
58	}
59
60	fn help_text(&self) -> Option<&str> {
61		self.help_text.as_deref()
62	}
63
64	fn widget(&self) -> &Widget {
65		&self.widget
66	}
67
68	fn initial(&self) -> Option<&serde_json::Value> {
69		self.initial.as_ref()
70	}
71
72	fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
73		match value {
74			None if self.required => Err(FieldError::required(None)),
75			None => Ok(serde_json::Value::String(String::new())),
76			Some(v) => {
77				let s = v
78					.as_str()
79					.ok_or_else(|| FieldError::Invalid("Expected string".to_string()))?;
80
81				let s = s.trim();
82
83				if s.is_empty() {
84					if self.required {
85						return Err(FieldError::required(None));
86					}
87					return Ok(serde_json::Value::String(String::new()));
88				}
89
90				// Check length using character count (not byte count)
91				// for correct multi-byte character handling
92				let char_count = s.chars().count();
93				if let Some(max) = self.max_length
94					&& char_count > max
95				{
96					return Err(FieldError::Validation(format!(
97						"Ensure this value has at most {} characters (it has {})",
98						max, char_count
99					)));
100				}
101
102				// Validate URL format
103				if !Self::validate_url(s) {
104					return Err(FieldError::Validation("Enter a valid URL".to_string()));
105				}
106
107				Ok(serde_json::Value::String(s.to_string()))
108			}
109		}
110	}
111}
112
113#[cfg(test)]
114mod tests {
115	use super::*;
116
117	#[test]
118	fn test_urlfield_valid() {
119		let field = URLField::new("website".to_string());
120
121		assert_eq!(
122			field
123				.clean(Some(&serde_json::json!("https://example.com")))
124				.unwrap(),
125			serde_json::json!("https://example.com")
126		);
127		assert_eq!(
128			field
129				.clean(Some(&serde_json::json!("http://test.org/path")))
130				.unwrap(),
131			serde_json::json!("http://test.org/path")
132		);
133	}
134
135	#[test]
136	fn test_urlfield_invalid() {
137		let field = URLField::new("website".to_string());
138
139		assert!(matches!(
140			field.clean(Some(&serde_json::json!("not a url"))),
141			Err(FieldError::Validation(_))
142		));
143		assert!(matches!(
144			field.clean(Some(&serde_json::json!("ftp://example.com"))),
145			Err(FieldError::Validation(_))
146		));
147	}
148
149	#[test]
150	fn test_urlfield_optional() {
151		let mut field = URLField::new("website".to_string());
152		field.required = false;
153
154		assert_eq!(field.clean(None).unwrap(), serde_json::json!(""));
155		assert_eq!(
156			field.clean(Some(&serde_json::json!(""))).unwrap(),
157			serde_json::json!("")
158		);
159	}
160}