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,
13 pub label: Option<String>,
15 pub required: bool,
17 pub help_text: Option<String>,
19 pub widget: Widget,
21 pub initial: Option<serde_json::Value>,
23 regex_cache: OnceLock<Regex>,
25 pattern: String,
27 pub error_message: String,
29 pub max_length: Option<usize>,
31 pub min_length: Option<usize>,
33}
34
35impl RegexField {
36 pub fn new(name: String, pattern: &str) -> Result<Self, regex::Error> {
49 let compiled = Regex::new(pattern)?;
51 let cache = OnceLock::new();
52 let _ = cache.set(compiled);
53 Ok(Self {
54 name,
55 label: None,
56 required: true,
57 help_text: None,
58 widget: Widget::TextInput,
59 initial: None,
60 regex_cache: cache,
61 pattern: pattern.to_string(),
62 error_message: "Enter a valid value".to_string(),
63 max_length: None,
64 min_length: None,
65 })
66 }
67
68 fn regex(&self) -> &Regex {
70 self.regex_cache.get_or_init(|| {
71 Regex::new(&self.pattern).expect("Pattern was validated at construction")
72 })
73 }
74 pub fn with_error_message(mut self, message: String) -> Self {
76 self.error_message = message;
77 self
78 }
79}
80
81impl std::fmt::Debug for RegexField {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("RegexField")
84 .field("name", &self.name)
85 .field("label", &self.label)
86 .field("required", &self.required)
87 .field("help_text", &self.help_text)
88 .field("widget", &self.widget)
89 .field("initial", &self.initial)
90 .field("pattern", &self.pattern)
91 .field("error_message", &self.error_message)
92 .field("max_length", &self.max_length)
93 .field("min_length", &self.min_length)
94 .finish()
95 }
96}
97
98impl Clone for RegexField {
99 fn clone(&self) -> Self {
100 let cache = OnceLock::new();
101 if let Some(regex) = self.regex_cache.get() {
102 let _ = cache.set(regex.clone());
103 }
104 Self {
105 name: self.name.clone(),
106 label: self.label.clone(),
107 required: self.required,
108 help_text: self.help_text.clone(),
109 widget: self.widget.clone(),
110 initial: self.initial.clone(),
111 regex_cache: cache,
112 pattern: self.pattern.clone(),
113 error_message: self.error_message.clone(),
114 max_length: self.max_length,
115 min_length: self.min_length,
116 }
117 }
118}
119
120impl FormField for RegexField {
121 fn name(&self) -> &str {
122 &self.name
123 }
124
125 fn label(&self) -> Option<&str> {
126 self.label.as_deref()
127 }
128
129 fn required(&self) -> bool {
130 self.required
131 }
132
133 fn help_text(&self) -> Option<&str> {
134 self.help_text.as_deref()
135 }
136
137 fn widget(&self) -> &Widget {
138 &self.widget
139 }
140
141 fn initial(&self) -> Option<&serde_json::Value> {
142 self.initial.as_ref()
143 }
144
145 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
146 match value {
147 None if self.required => Err(FieldError::required(None)),
148 None => Ok(serde_json::Value::Null),
149 Some(v) => {
150 let s = v
151 .as_str()
152 .ok_or_else(|| FieldError::invalid(None, "Expected string"))?;
153
154 if s.is_empty() {
155 if self.required {
156 return Err(FieldError::required(None));
157 }
158 return Ok(serde_json::Value::Null);
159 }
160
161 let char_count = s.chars().count();
164 if let Some(max) = self.max_length
165 && char_count > max
166 {
167 return Err(FieldError::validation(
168 None,
169 &format!("Ensure this value has at most {} characters", max),
170 ));
171 }
172
173 if let Some(min) = self.min_length
174 && char_count < min
175 {
176 return Err(FieldError::validation(
177 None,
178 &format!("Ensure this value has at least {} characters", min),
179 ));
180 }
181
182 if !self.regex().is_match(s) {
184 return Err(FieldError::validation(None, &self.error_message));
185 }
186
187 Ok(serde_json::Value::String(s.to_string()))
188 }
189 }
190 }
191}
192
193#[derive(Debug, Clone)]
195pub struct SlugField {
196 pub name: String,
198 pub label: Option<String>,
200 pub required: bool,
202 pub help_text: Option<String>,
204 pub widget: Widget,
206 pub initial: Option<serde_json::Value>,
208 pub max_length: Option<usize>,
210 pub allow_unicode: bool,
212}
213
214impl SlugField {
215 pub fn new(name: String) -> Self {
217 Self {
218 name,
219 label: None,
220 required: true,
221 help_text: None,
222 widget: Widget::TextInput,
223 initial: None,
224 max_length: Some(50),
225 allow_unicode: false,
226 }
227 }
228
229 fn is_valid_slug(&self, s: &str) -> bool {
230 if self.allow_unicode {
231 s.chars().all(|c| {
232 c.is_alphanumeric() || c == '-' || c == '_' || (!c.is_ascii() && c.is_alphabetic())
233 })
234 } else {
235 s.chars()
236 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
237 }
238 }
239}
240
241impl FormField for SlugField {
242 fn name(&self) -> &str {
243 &self.name
244 }
245
246 fn label(&self) -> Option<&str> {
247 self.label.as_deref()
248 }
249
250 fn required(&self) -> bool {
251 self.required
252 }
253
254 fn help_text(&self) -> Option<&str> {
255 self.help_text.as_deref()
256 }
257
258 fn widget(&self) -> &Widget {
259 &self.widget
260 }
261
262 fn initial(&self) -> Option<&serde_json::Value> {
263 self.initial.as_ref()
264 }
265
266 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
267 match value {
268 None if self.required => Err(FieldError::required(None)),
269 None => Ok(serde_json::Value::Null),
270 Some(v) => {
271 let s = v
272 .as_str()
273 .ok_or_else(|| FieldError::invalid(None, "Expected string"))?;
274
275 if s.is_empty() {
276 if self.required {
277 return Err(FieldError::required(None));
278 }
279 return Ok(serde_json::Value::Null);
280 }
281
282 if let Some(max) = self.max_length
284 && s.chars().count() > max
285 {
286 return Err(FieldError::validation(
287 None,
288 &format!("Ensure this value has at most {} characters", max),
289 ));
290 }
291
292 if !self.is_valid_slug(s) {
293 let msg = if self.allow_unicode {
294 "Enter a valid slug consisting of Unicode letters, numbers, underscores, or hyphens"
295 } else {
296 "Enter a valid slug consisting of letters, numbers, underscores or hyphens"
297 };
298 return Err(FieldError::validation(None, msg));
299 }
300
301 Ok(serde_json::Value::String(s.to_string()))
302 }
303 }
304 }
305}
306
307#[derive(Debug, Clone)]
309pub struct GenericIPAddressField {
310 pub name: String,
312 pub label: Option<String>,
314 pub required: bool,
316 pub help_text: Option<String>,
318 pub widget: Widget,
320 pub initial: Option<serde_json::Value>,
322 pub protocol: IPProtocol,
324}
325
326#[derive(Debug, Clone, Copy)]
328pub enum IPProtocol {
329 Both,
331 IPv4,
333 IPv6,
335}
336
337impl GenericIPAddressField {
338 pub fn new(name: String) -> Self {
340 Self {
341 name,
342 label: None,
343 required: true,
344 help_text: None,
345 widget: Widget::TextInput,
346 initial: None,
347 protocol: IPProtocol::Both,
348 }
349 }
350
351 fn is_valid_ipv4(&self, s: &str) -> bool {
352 let parts: Vec<&str> = s.split('.').collect();
353 if parts.len() != 4 {
354 return false;
355 }
356
357 parts.iter().all(|part| {
358 part.parse::<u8>()
359 .map(|_| !part.starts_with('0') || part.len() == 1)
360 .unwrap_or(false)
361 })
362 }
363
364 fn is_valid_ipv6(&self, s: &str) -> bool {
365 s.parse::<Ipv6Addr>().is_ok()
369 }
370}
371
372impl FormField for GenericIPAddressField {
373 fn name(&self) -> &str {
374 &self.name
375 }
376
377 fn label(&self) -> Option<&str> {
378 self.label.as_deref()
379 }
380
381 fn required(&self) -> bool {
382 self.required
383 }
384
385 fn help_text(&self) -> Option<&str> {
386 self.help_text.as_deref()
387 }
388
389 fn widget(&self) -> &Widget {
390 &self.widget
391 }
392
393 fn initial(&self) -> Option<&serde_json::Value> {
394 self.initial.as_ref()
395 }
396
397 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
398 match value {
399 None if self.required => Err(FieldError::required(None)),
400 None => Ok(serde_json::Value::Null),
401 Some(v) => {
402 let s = v
403 .as_str()
404 .ok_or_else(|| FieldError::invalid(None, "Expected string"))?;
405
406 if s.is_empty() {
407 if self.required {
408 return Err(FieldError::required(None));
409 }
410 return Ok(serde_json::Value::Null);
411 }
412
413 let is_valid = match self.protocol {
414 IPProtocol::IPv4 => self.is_valid_ipv4(s),
415 IPProtocol::IPv6 => self.is_valid_ipv6(s),
416 IPProtocol::Both => self.is_valid_ipv4(s) || self.is_valid_ipv6(s),
417 };
418
419 if !is_valid {
420 return Err(FieldError::validation(None, "Enter a valid IP address"));
421 }
422
423 Ok(serde_json::Value::String(s.to_string()))
424 }
425 }
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use rstest::rstest;
433
434 #[test]
435 fn test_regex_field() {
436 let field = RegexField::new("code".to_string(), r"^[A-Z]{3}\d{3}$").unwrap();
437
438 assert!(field.clean(Some(&serde_json::json!("ABC123"))).is_ok());
439 assert!(matches!(
440 field.clean(Some(&serde_json::json!("abc123"))),
441 Err(FieldError::Validation(_))
442 ));
443 }
444
445 #[test]
446 fn test_forms_regex_field_slug() {
447 let field = SlugField::new("slug".to_string());
448
449 assert!(field.clean(Some(&serde_json::json!("my-slug"))).is_ok());
450 assert!(field.clean(Some(&serde_json::json!("my_slug"))).is_ok());
451 assert!(matches!(
452 field.clean(Some(&serde_json::json!("my slug"))),
453 Err(FieldError::Validation(_))
454 ));
455 }
456
457 #[test]
458 fn test_ip_field_ipv4() {
459 let mut field = GenericIPAddressField::new("ip".to_string());
460 field.protocol = IPProtocol::IPv4;
461
462 assert!(field.clean(Some(&serde_json::json!("192.168.1.1"))).is_ok());
463 assert!(matches!(
464 field.clean(Some(&serde_json::json!("999.999.999.999"))),
465 Err(FieldError::Validation(_))
466 ));
467 }
468
469 #[test]
470 fn test_ip_field_ipv6() {
471 let mut field = GenericIPAddressField::new("ip".to_string());
472 field.protocol = IPProtocol::IPv6;
473
474 assert!(
475 field
476 .clean(Some(&serde_json::json!(
477 "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
478 )))
479 .is_ok()
480 );
481 assert!(field.clean(Some(&serde_json::json!("::1"))).is_ok());
482 }
483
484 #[rstest]
485 #[case("::1", true)]
486 #[case("::", true)]
487 #[case("::ffff:192.0.2.1", true)]
488 #[case("2001:db8::1", true)]
489 #[case("fe80::1%eth0", false)]
490 #[case("2001:db8:85a3::8a2e:370:7334", true)]
491 #[case("::ffff:10.0.0.1", true)]
492 #[case("2001:db8::", true)]
493 #[case("::192.168.1.1", true)]
494 #[case("not-an-ip", false)]
495 #[case("2001:db8::g1", false)]
496 #[case("12345::1", false)]
497 fn test_ipv6_comprehensive_validation(#[case] input: &str, #[case] should_accept: bool) {
498 let mut field = GenericIPAddressField::new("ip".to_string());
500 field.protocol = IPProtocol::IPv6;
501
502 let result = field.clean(Some(&serde_json::json!(input)));
504
505 if should_accept {
507 assert!(
508 result.is_ok(),
509 "Expected valid IPv6 '{}' to be accepted, got: {:?}",
510 input,
511 result,
512 );
513 } else {
514 assert!(
515 result.is_err(),
516 "Expected invalid IPv6 '{}' to be rejected, got: {:?}",
517 input,
518 result,
519 );
520 }
521 }
522}