karbon_framework/validation/constraints/string/
ip.rs1use crate::validation::constraints::{Constraint, ConstraintResult, ConstraintViolation};
2
3#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum IpVersion {
6 V4,
7 V6,
8 All,
9}
10
11pub struct Ip {
13 pub message: String,
14 pub version: IpVersion,
15}
16
17impl Default for Ip {
18 fn default() -> Self {
19 Self {
20 message: "This value is not a valid IP address.".to_string(),
21 version: IpVersion::All,
22 }
23 }
24}
25
26impl Ip {
27 pub fn new() -> Self {
28 Self::default()
29 }
30
31 pub fn v4() -> Self {
32 Self {
33 version: IpVersion::V4,
34 ..Self::default()
35 }
36 }
37
38 pub fn v6() -> Self {
39 Self {
40 version: IpVersion::V6,
41 ..Self::default()
42 }
43 }
44
45 pub fn with_message(mut self, message: impl Into<String>) -> Self {
46 self.message = message.into();
47 self
48 }
49
50 fn is_valid_ipv4(value: &str) -> bool {
51 let parts: Vec<&str> = value.split('.').collect();
52 if parts.len() != 4 {
53 return false;
54 }
55 for part in parts {
56 if part.is_empty() || part.len() > 3 {
57 return false;
58 }
59 if part.len() > 1 && part.starts_with('0') {
61 return false;
62 }
63 match part.parse::<u16>() {
64 Ok(n) if n <= 255 => {}
65 _ => return false,
66 }
67 }
68 true
69 }
70
71 fn is_valid_ipv6(value: &str) -> bool {
72 if value.is_empty() {
73 return false;
74 }
75
76 if value.contains(":::") {
78 return false;
79 }
80
81 let double_colon_count = value.matches("::").count();
83 if double_colon_count > 1 {
84 return false;
85 }
86
87 let groups: Vec<&str> = value.split(':').collect();
88
89 if double_colon_count == 1 {
90 if groups.len() > 8 {
91 return false;
92 }
93 } else if groups.len() != 8 {
94 return false;
95 }
96
97 for group in &groups {
98 if group.is_empty() {
99 continue; }
101 if group.len() > 4 {
102 return false;
103 }
104 if !group.chars().all(|c| c.is_ascii_hexdigit()) {
105 return false;
106 }
107 }
108
109 true
110 }
111
112 fn is_valid(&self, value: &str) -> bool {
113 match self.version {
114 IpVersion::V4 => Self::is_valid_ipv4(value),
115 IpVersion::V6 => Self::is_valid_ipv6(value),
116 IpVersion::All => Self::is_valid_ipv4(value) || Self::is_valid_ipv6(value),
117 }
118 }
119}
120
121impl Constraint for Ip {
122 fn validate(&self, value: &str) -> ConstraintResult {
123 if !self.is_valid(value) {
124 return Err(ConstraintViolation::new(
125 self.name(),
126 &self.message,
127 value,
128 ));
129 }
130 Ok(())
131 }
132
133 fn name(&self) -> &'static str {
134 "Ip"
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_valid_ipv4() {
144 let constraint = Ip::v4();
145 assert!(constraint.validate("127.0.0.1").is_ok());
146 assert!(constraint.validate("192.168.1.1").is_ok());
147 assert!(constraint.validate("0.0.0.0").is_ok());
148 assert!(constraint.validate("255.255.255.255").is_ok());
149 }
150
151 #[test]
152 fn test_invalid_ipv4() {
153 let constraint = Ip::v4();
154 assert!(constraint.validate("256.0.0.1").is_err());
155 assert!(constraint.validate("1.2.3").is_err());
156 assert!(constraint.validate("1.2.3.4.5").is_err());
157 assert!(constraint.validate("01.02.03.04").is_err()); assert!(constraint.validate("abc.def.ghi.jkl").is_err());
159 }
160
161 #[test]
162 fn test_valid_ipv6() {
163 let constraint = Ip::v6();
164 assert!(constraint.validate("::1").is_ok());
165 assert!(constraint.validate("2001:0db8:85a3:0000:0000:8a2e:0370:7334").is_ok());
166 assert!(constraint.validate("fe80::1").is_ok());
167 assert!(constraint.validate("::").is_ok());
168 }
169
170 #[test]
171 fn test_invalid_ipv6() {
172 let constraint = Ip::v6();
173 assert!(constraint.validate("127.0.0.1").is_err());
174 assert!(constraint.validate(":::1").is_err());
175 assert!(constraint.validate("2001:db8::85a3::7334").is_err()); }
177
178 #[test]
179 fn test_all_versions() {
180 let constraint = Ip::new();
181 assert!(constraint.validate("127.0.0.1").is_ok());
182 assert!(constraint.validate("::1").is_ok());
183 assert!(constraint.validate("not-an-ip").is_err());
184 }
185}