cloudillo_types/
address.rs1use crate::prelude::*;
7use serde::{Deserialize, Serialize};
8use std::net::{Ipv4Addr, Ipv6Addr};
9use std::str::FromStr;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum AddressType {
14 Ipv4,
16 Ipv6,
18 Hostname,
20}
21
22impl std::fmt::Display for AddressType {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 match self {
25 AddressType::Ipv4 => write!(f, "ipv4"),
26 AddressType::Ipv6 => write!(f, "ipv6"),
27 AddressType::Hostname => write!(f, "hostname"),
28 }
29 }
30}
31
32pub fn parse_address_type(address: &str) -> ClResult<AddressType> {
36 if Ipv4Addr::from_str(address).is_ok() {
38 return Ok(AddressType::Ipv4);
39 }
40
41 if Ipv6Addr::from_str(address).is_ok() {
43 return Ok(AddressType::Ipv6);
44 }
45
46 if address.is_empty() {
50 return Err(Error::ValidationError("Address cannot be empty".to_string()));
51 }
52
53 if address.len() > 253 {
54 return Err(Error::ValidationError("Hostname too long (max 253 characters)".to_string()));
55 }
56
57 let valid_chars = |c: char| c.is_alphanumeric() || c == '.' || c == '-' || c == '_';
59 if !address.chars().all(valid_chars) {
60 return Err(Error::ValidationError(
61 "Invalid hostname characters (allowed: alphanumeric, dot, hyphen, underscore)"
62 .to_string(),
63 ));
64 }
65
66 for label in address.split('.') {
68 if label.is_empty() {
69 return Err(Error::ValidationError("Hostname labels cannot be empty".to_string()));
70 }
71 if label.starts_with('-') || label.ends_with('-') {
72 return Err(Error::ValidationError(
73 "Hostname labels cannot start or end with hyphen".to_string(),
74 ));
75 }
76 if label.len() > 63 {
77 return Err(Error::ValidationError(
78 "Hostname label too long (max 63 characters)".to_string(),
79 ));
80 }
81 }
82
83 Ok(AddressType::Hostname)
84}
85
86pub fn validate_address_type_consistency(addresses: &[Box<str>]) -> ClResult<Option<AddressType>> {
89 if addresses.is_empty() {
91 return Ok(None);
92 }
93
94 let first_type = parse_address_type(addresses[0].as_ref())?;
96
97 for (i, addr) in addresses.iter().enumerate().skip(1) {
99 let addr_type = parse_address_type(addr.as_ref())?;
100 if addr_type != first_type {
101 return Err(Error::ValidationError(format!(
102 "Address type mismatch: address[0] is {}, but address[{}] is {}",
103 first_type, i, addr_type
104 )));
105 }
106 }
107
108 Ok(Some(first_type))
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn test_ipv4_detection() {
117 assert_eq!(parse_address_type("192.168.1.1").ok(), Some(AddressType::Ipv4));
118 assert_eq!(parse_address_type("203.0.113.42").ok(), Some(AddressType::Ipv4));
119 assert_eq!(parse_address_type("0.0.0.0").ok(), Some(AddressType::Ipv4));
120 assert_eq!(parse_address_type("255.255.255.255").ok(), Some(AddressType::Ipv4));
121 }
122
123 #[test]
124 fn test_ipv6_detection() {
125 assert_eq!(parse_address_type("2001:db8::1").ok(), Some(AddressType::Ipv6));
126 assert_eq!(parse_address_type("::1").ok(), Some(AddressType::Ipv6));
127 assert_eq!(parse_address_type("::").ok(), Some(AddressType::Ipv6));
128 assert_eq!(parse_address_type("fe80::1").ok(), Some(AddressType::Ipv6));
129 }
130
131 #[test]
132 fn test_hostname_detection() {
133 assert_eq!(parse_address_type("example.com").ok(), Some(AddressType::Hostname));
134 assert_eq!(parse_address_type("server.cloudillo.net").ok(), Some(AddressType::Hostname));
135 assert_eq!(parse_address_type("api-server").ok(), Some(AddressType::Hostname));
136 assert_eq!(parse_address_type("my_server").ok(), Some(AddressType::Hostname));
137 }
138
139 #[test]
140 fn test_hostname_validation_errors() {
141 assert!(parse_address_type("").is_err());
143
144 assert!(parse_address_type(&"a".repeat(254)).is_err());
146
147 assert!(parse_address_type("example.com/path").is_err());
149 assert!(parse_address_type("example@com").is_err());
150
151 assert!(parse_address_type("example..com").is_err());
153 assert!(parse_address_type(".example.com").is_err());
154 assert!(parse_address_type("example.com.").is_err());
155
156 assert!(parse_address_type("-example.com").is_err());
158 assert!(parse_address_type("example.com-").is_err());
159 assert!(parse_address_type("example.-com").is_err());
160
161 assert!(parse_address_type(&format!("{}.com", "a".repeat(64))).is_err());
163 }
164
165 #[test]
166 fn test_address_type_consistency_empty() {
167 let addresses: Vec<Box<str>> = vec![];
168 assert!(validate_address_type_consistency(&addresses).is_ok());
169 assert_eq!(validate_address_type_consistency(&addresses).ok(), Some(None));
170 }
171
172 #[test]
173 fn test_address_type_consistency_single() {
174 let addresses = vec!["192.168.1.1".into()];
175 let result = validate_address_type_consistency(&addresses);
176 assert!(result.is_ok());
177 assert_eq!(result.ok(), Some(Some(AddressType::Ipv4)));
178 }
179
180 #[test]
181 fn test_address_type_consistency_mixed() {
182 let addresses = vec!["192.168.1.1".into(), "2001:db8::1".into()];
183 assert!(validate_address_type_consistency(&addresses).is_err());
184 }
185}
186
187