karbon_framework/validation/constraints/string/
hostname.rs1use crate::validation::constraints::{Constraint, ConstraintResult, ConstraintViolation};
2
3pub struct Hostname {
5 pub message: String,
6 pub require_tld: bool,
7}
8
9impl Default for Hostname {
10 fn default() -> Self {
11 Self {
12 message: "This value is not a valid hostname.".to_string(),
13 require_tld: true,
14 }
15 }
16}
17
18impl Hostname {
19 pub fn new() -> Self {
20 Self::default()
21 }
22
23 pub fn with_message(mut self, message: impl Into<String>) -> Self {
24 self.message = message.into();
25 self
26 }
27
28 pub fn require_tld(mut self, require: bool) -> Self {
29 self.require_tld = require;
30 self
31 }
32
33 fn is_valid_hostname(&self, value: &str) -> bool {
34 if value.is_empty() || value.len() > 253 {
35 return false;
36 }
37
38 let labels: Vec<&str> = value.split('.').collect();
39
40 if self.require_tld && labels.len() < 2 {
41 return false;
42 }
43
44 for label in &labels {
45 if label.is_empty() || label.len() > 63 {
46 return false;
47 }
48 if label.starts_with('-') || label.ends_with('-') {
49 return false;
50 }
51 if !label.chars().all(|c| c.is_alphanumeric() || c == '-') {
52 return false;
53 }
54 }
55
56 if self.require_tld {
58 if let Some(tld) = labels.last() {
59 if tld.chars().all(|c| c.is_ascii_digit()) {
60 return false;
61 }
62 }
63 }
64
65 true
66 }
67}
68
69impl Constraint for Hostname {
70 fn validate(&self, value: &str) -> ConstraintResult {
71 if !self.is_valid_hostname(value) {
72 return Err(ConstraintViolation::new(
73 self.name(),
74 &self.message,
75 value,
76 ));
77 }
78 Ok(())
79 }
80
81 fn name(&self) -> &'static str {
82 "Hostname"
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn test_valid_hostnames() {
92 let constraint = Hostname::new();
93 assert!(constraint.validate("example.com").is_ok());
94 assert!(constraint.validate("sub.example.com").is_ok());
95 assert!(constraint.validate("my-host.example.org").is_ok());
96 assert!(constraint.validate("a.io").is_ok());
97 }
98
99 #[test]
100 fn test_invalid_hostnames() {
101 let constraint = Hostname::new();
102 assert!(constraint.validate("").is_err());
103 assert!(constraint.validate("-example.com").is_err());
104 assert!(constraint.validate("example-.com").is_err());
105 assert!(constraint.validate("exam ple.com").is_err());
106 assert!(constraint.validate("example.123").is_err()); }
108
109 #[test]
110 fn test_without_tld_requirement() {
111 let constraint = Hostname::new().require_tld(false);
112 assert!(constraint.validate("localhost").is_ok());
113 assert!(constraint.validate("myserver").is_ok());
114 }
115
116 #[test]
117 fn test_with_tld_requirement() {
118 let constraint = Hostname::new();
119 assert!(constraint.validate("localhost").is_err());
120 }
121}