reinhardt_forms/fields/
url_field.rs1use crate::field::{FieldError, FieldResult, FormField, Widget};
2
3pub 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 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 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 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 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}