karbon_framework/validation/constraints/string/
url.rs1use crate::validation::constraints::{Constraint, ConstraintResult, ConstraintViolation};
2
3pub struct Url {
7 pub message: String,
8 pub protocols: Vec<String>,
9}
10
11impl Default for Url {
12 fn default() -> Self {
13 Self {
14 message: "This value is not a valid URL.".to_string(),
15 protocols: vec!["http".to_string(), "https".to_string()],
16 }
17 }
18}
19
20impl Url {
21 pub fn new() -> Self {
22 Self::default()
23 }
24
25 pub fn with_message(mut self, message: impl Into<String>) -> Self {
26 self.message = message.into();
27 self
28 }
29
30 pub fn with_protocols(mut self, protocols: Vec<String>) -> Self {
31 self.protocols = protocols;
32 self
33 }
34
35 fn is_valid_url(&self, value: &str) -> bool {
36 let protocol_end = match value.find("://") {
38 Some(pos) => pos,
39 None => return false,
40 };
41
42 let protocol = &value[..protocol_end];
43 if !self.protocols.iter().any(|p| p == protocol) {
44 return false;
45 }
46
47 let rest = &value[protocol_end + 3..];
48 if rest.is_empty() {
49 return false;
50 }
51
52 let host_end = rest.find('/').or_else(|| rest.find('?')).or_else(|| rest.find('#')).unwrap_or(rest.len());
54 let host_part = &rest[..host_end];
55
56 if host_part.is_empty() {
57 return false;
58 }
59
60 let host = if let Some(colon_pos) = host_part.rfind(':') {
62 let port_str = &host_part[colon_pos + 1..];
63 if !port_str.is_empty() && port_str.parse::<u16>().is_err() {
64 return false;
65 }
66 &host_part[..colon_pos]
67 } else {
68 host_part
69 };
70
71 if host.is_empty() {
72 return false;
73 }
74
75 for part in host.split('.') {
77 if part.is_empty() || part.len() > 63 {
78 return false;
79 }
80 }
81
82 true
83 }
84}
85
86impl Constraint for Url {
87 fn validate(&self, value: &str) -> ConstraintResult {
88 if !self.is_valid_url(value) {
89 return Err(ConstraintViolation::new(
90 self.name(),
91 &self.message,
92 value,
93 ));
94 }
95 Ok(())
96 }
97
98 fn name(&self) -> &'static str {
99 "Url"
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn test_valid_urls() {
109 let constraint = Url::new();
110 assert!(constraint.validate("http://example.com").is_ok());
111 assert!(constraint.validate("https://example.com").is_ok());
112 assert!(constraint.validate("https://example.com/path").is_ok());
113 assert!(constraint.validate("https://example.com:8080").is_ok());
114 assert!(constraint.validate("https://sub.domain.example.com").is_ok());
115 assert!(constraint.validate("https://example.com/path?query=1").is_ok());
116 assert!(constraint.validate("https://example.com/path#fragment").is_ok());
117 }
118
119 #[test]
120 fn test_invalid_urls() {
121 let constraint = Url::new();
122 assert!(constraint.validate("").is_err());
123 assert!(constraint.validate("not-a-url").is_err());
124 assert!(constraint.validate("ftp://example.com").is_err()); assert!(constraint.validate("://example.com").is_err());
126 assert!(constraint.validate("http://").is_err());
127 }
128
129 #[test]
130 fn test_custom_protocols() {
131 let constraint = Url::new().with_protocols(vec!["ftp".to_string(), "ssh".to_string()]);
132 assert!(constraint.validate("ftp://example.com").is_ok());
133 assert!(constraint.validate("ssh://example.com").is_ok());
134 assert!(constraint.validate("http://example.com").is_err());
135 }
136}