1use crate::field::{FieldError, FieldResult, FormField, Widget};
2use regex::Regex;
3use std::net::Ipv6Addr;
4use std::sync::OnceLock;
5
6pub struct RegexField {
11 pub name: String,
12 pub label: Option<String>,
13 pub required: bool,
14 pub help_text: Option<String>,
15 pub widget: Widget,
16 pub initial: Option<serde_json::Value>,
17 regex_cache: OnceLock<Regex>,
19 pattern: String,
21 pub error_message: String,
22 pub max_length: Option<usize>,
23 pub min_length: Option<usize>,
24}
25
26impl RegexField {
27 pub fn new(name: String, pattern: &str) -> Result<Self, regex::Error> {
40 let compiled = Regex::new(pattern)?;
42 let cache = OnceLock::new();
43 cache
44 .set(compiled)
45 .unwrap_or_else(|_| panic!("OnceLock should be empty at construction"));
46 Ok(Self {
47 name,
48 label: None,
49 required: true,
50 help_text: None,
51 widget: Widget::TextInput,
52 initial: None,
53 regex_cache: cache,
54 pattern: pattern.to_string(),
55 error_message: "Enter a valid value".to_string(),
56 max_length: None,
57 min_length: None,
58 })
59 }
60
61 fn regex(&self) -> &Regex {
63 self.regex_cache.get_or_init(|| {
64 Regex::new(&self.pattern).expect("Pattern was validated at construction")
65 })
66 }
67 pub fn with_error_message(mut self, message: String) -> Self {
68 self.error_message = message;
69 self
70 }
71}
72
73impl FormField for RegexField {
74 fn name(&self) -> &str {
75 &self.name
76 }
77
78 fn label(&self) -> Option<&str> {
79 self.label.as_deref()
80 }
81
82 fn required(&self) -> bool {
83 self.required
84 }
85
86 fn help_text(&self) -> Option<&str> {
87 self.help_text.as_deref()
88 }
89
90 fn widget(&self) -> &Widget {
91 &self.widget
92 }
93
94 fn initial(&self) -> Option<&serde_json::Value> {
95 self.initial.as_ref()
96 }
97
98 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
99 match value {
100 None if self.required => Err(FieldError::required(None)),
101 None => Ok(serde_json::Value::Null),
102 Some(v) => {
103 let s = v
104 .as_str()
105 .ok_or_else(|| FieldError::invalid(None, "Expected string"))?;
106
107 if s.is_empty() {
108 if self.required {
109 return Err(FieldError::required(None));
110 }
111 return Ok(serde_json::Value::Null);
112 }
113
114 let char_count = s.chars().count();
117 if let Some(max) = self.max_length
118 && char_count > max
119 {
120 return Err(FieldError::validation(
121 None,
122 &format!("Ensure this value has at most {} characters", max),
123 ));
124 }
125
126 if let Some(min) = self.min_length
127 && char_count < min
128 {
129 return Err(FieldError::validation(
130 None,
131 &format!("Ensure this value has at least {} characters", min),
132 ));
133 }
134
135 if !self.regex().is_match(s) {
137 return Err(FieldError::validation(None, &self.error_message));
138 }
139
140 Ok(serde_json::Value::String(s.to_string()))
141 }
142 }
143 }
144}
145
146pub struct SlugField {
148 pub name: String,
149 pub label: Option<String>,
150 pub required: bool,
151 pub help_text: Option<String>,
152 pub widget: Widget,
153 pub initial: Option<serde_json::Value>,
154 pub max_length: Option<usize>,
155 pub allow_unicode: bool,
156}
157
158impl SlugField {
159 pub fn new(name: String) -> Self {
160 Self {
161 name,
162 label: None,
163 required: true,
164 help_text: None,
165 widget: Widget::TextInput,
166 initial: None,
167 max_length: Some(50),
168 allow_unicode: false,
169 }
170 }
171
172 fn is_valid_slug(&self, s: &str) -> bool {
173 if self.allow_unicode {
174 s.chars().all(|c| {
175 c.is_alphanumeric() || c == '-' || c == '_' || (!c.is_ascii() && c.is_alphabetic())
176 })
177 } else {
178 s.chars()
179 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
180 }
181 }
182}
183
184impl FormField for SlugField {
185 fn name(&self) -> &str {
186 &self.name
187 }
188
189 fn label(&self) -> Option<&str> {
190 self.label.as_deref()
191 }
192
193 fn required(&self) -> bool {
194 self.required
195 }
196
197 fn help_text(&self) -> Option<&str> {
198 self.help_text.as_deref()
199 }
200
201 fn widget(&self) -> &Widget {
202 &self.widget
203 }
204
205 fn initial(&self) -> Option<&serde_json::Value> {
206 self.initial.as_ref()
207 }
208
209 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
210 match value {
211 None if self.required => Err(FieldError::required(None)),
212 None => Ok(serde_json::Value::Null),
213 Some(v) => {
214 let s = v
215 .as_str()
216 .ok_or_else(|| FieldError::invalid(None, "Expected string"))?;
217
218 if s.is_empty() {
219 if self.required {
220 return Err(FieldError::required(None));
221 }
222 return Ok(serde_json::Value::Null);
223 }
224
225 if let Some(max) = self.max_length
227 && s.chars().count() > max
228 {
229 return Err(FieldError::validation(
230 None,
231 &format!("Ensure this value has at most {} characters", max),
232 ));
233 }
234
235 if !self.is_valid_slug(s) {
236 let msg = if self.allow_unicode {
237 "Enter a valid slug consisting of Unicode letters, numbers, underscores, or hyphens"
238 } else {
239 "Enter a valid slug consisting of letters, numbers, underscores or hyphens"
240 };
241 return Err(FieldError::validation(None, msg));
242 }
243
244 Ok(serde_json::Value::String(s.to_string()))
245 }
246 }
247 }
248}
249
250pub struct GenericIPAddressField {
252 pub name: String,
253 pub label: Option<String>,
254 pub required: bool,
255 pub help_text: Option<String>,
256 pub widget: Widget,
257 pub initial: Option<serde_json::Value>,
258 pub protocol: IPProtocol,
259}
260
261#[derive(Debug, Clone, Copy)]
262pub enum IPProtocol {
263 Both,
264 IPv4,
265 IPv6,
266}
267
268impl GenericIPAddressField {
269 pub fn new(name: String) -> Self {
270 Self {
271 name,
272 label: None,
273 required: true,
274 help_text: None,
275 widget: Widget::TextInput,
276 initial: None,
277 protocol: IPProtocol::Both,
278 }
279 }
280
281 fn is_valid_ipv4(&self, s: &str) -> bool {
282 let parts: Vec<&str> = s.split('.').collect();
283 if parts.len() != 4 {
284 return false;
285 }
286
287 parts.iter().all(|part| {
288 part.parse::<u8>()
289 .map(|_| !part.starts_with('0') || part.len() == 1)
290 .unwrap_or(false)
291 })
292 }
293
294 fn is_valid_ipv6(&self, s: &str) -> bool {
295 s.parse::<Ipv6Addr>().is_ok()
299 }
300}
301
302impl FormField for GenericIPAddressField {
303 fn name(&self) -> &str {
304 &self.name
305 }
306
307 fn label(&self) -> Option<&str> {
308 self.label.as_deref()
309 }
310
311 fn required(&self) -> bool {
312 self.required
313 }
314
315 fn help_text(&self) -> Option<&str> {
316 self.help_text.as_deref()
317 }
318
319 fn widget(&self) -> &Widget {
320 &self.widget
321 }
322
323 fn initial(&self) -> Option<&serde_json::Value> {
324 self.initial.as_ref()
325 }
326
327 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
328 match value {
329 None if self.required => Err(FieldError::required(None)),
330 None => Ok(serde_json::Value::Null),
331 Some(v) => {
332 let s = v
333 .as_str()
334 .ok_or_else(|| FieldError::invalid(None, "Expected string"))?;
335
336 if s.is_empty() {
337 if self.required {
338 return Err(FieldError::required(None));
339 }
340 return Ok(serde_json::Value::Null);
341 }
342
343 let is_valid = match self.protocol {
344 IPProtocol::IPv4 => self.is_valid_ipv4(s),
345 IPProtocol::IPv6 => self.is_valid_ipv6(s),
346 IPProtocol::Both => self.is_valid_ipv4(s) || self.is_valid_ipv6(s),
347 };
348
349 if !is_valid {
350 return Err(FieldError::validation(None, "Enter a valid IP address"));
351 }
352
353 Ok(serde_json::Value::String(s.to_string()))
354 }
355 }
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use rstest::rstest;
363
364 #[test]
365 fn test_regex_field() {
366 let field = RegexField::new("code".to_string(), r"^[A-Z]{3}\d{3}$").unwrap();
367
368 assert!(field.clean(Some(&serde_json::json!("ABC123"))).is_ok());
369 assert!(matches!(
370 field.clean(Some(&serde_json::json!("abc123"))),
371 Err(FieldError::Validation(_))
372 ));
373 }
374
375 #[test]
376 fn test_forms_regex_field_slug() {
377 let field = SlugField::new("slug".to_string());
378
379 assert!(field.clean(Some(&serde_json::json!("my-slug"))).is_ok());
380 assert!(field.clean(Some(&serde_json::json!("my_slug"))).is_ok());
381 assert!(matches!(
382 field.clean(Some(&serde_json::json!("my slug"))),
383 Err(FieldError::Validation(_))
384 ));
385 }
386
387 #[test]
388 fn test_ip_field_ipv4() {
389 let mut field = GenericIPAddressField::new("ip".to_string());
390 field.protocol = IPProtocol::IPv4;
391
392 assert!(field.clean(Some(&serde_json::json!("192.168.1.1"))).is_ok());
393 assert!(matches!(
394 field.clean(Some(&serde_json::json!("999.999.999.999"))),
395 Err(FieldError::Validation(_))
396 ));
397 }
398
399 #[test]
400 fn test_ip_field_ipv6() {
401 let mut field = GenericIPAddressField::new("ip".to_string());
402 field.protocol = IPProtocol::IPv6;
403
404 assert!(
405 field
406 .clean(Some(&serde_json::json!(
407 "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
408 )))
409 .is_ok()
410 );
411 assert!(field.clean(Some(&serde_json::json!("::1"))).is_ok());
412 }
413
414 #[rstest]
415 #[case("::1", true)]
416 #[case("::", true)]
417 #[case("::ffff:192.0.2.1", true)]
418 #[case("2001:db8::1", true)]
419 #[case("fe80::1%eth0", false)]
420 #[case("2001:db8:85a3::8a2e:370:7334", true)]
421 #[case("::ffff:10.0.0.1", true)]
422 #[case("2001:db8::", true)]
423 #[case("::192.168.1.1", true)]
424 #[case("not-an-ip", false)]
425 #[case("2001:db8::g1", false)]
426 #[case("12345::1", false)]
427 fn test_ipv6_comprehensive_validation(#[case] input: &str, #[case] should_accept: bool) {
428 let mut field = GenericIPAddressField::new("ip".to_string());
430 field.protocol = IPProtocol::IPv6;
431
432 let result = field.clean(Some(&serde_json::json!(input)));
434
435 if should_accept {
437 assert!(
438 result.is_ok(),
439 "Expected valid IPv6 '{}' to be accepted, got: {:?}",
440 input,
441 result,
442 );
443 } else {
444 assert!(
445 result.is_err(),
446 "Expected invalid IPv6 '{}' to be rejected, got: {:?}",
447 input,
448 result,
449 );
450 }
451 }
452}