Skip to main content

karbon_framework/validation/constraints/string/
url.rs

1use crate::validation::constraints::{Constraint, ConstraintResult, ConstraintViolation};
2
3/// Validates that a value is a valid URL.
4///
5/// Equivalent to Symfony's `Url` constraint.
6pub 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        // Check protocol
37        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        // Extract host (before path, query, fragment)
53        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        // Handle port
61        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        // Validate host
76        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()); // ftp not in default protocols
125        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}