use std::{fmt, str::FromStr}; #[derive(Clone, Debug, Default, PartialEq)] pub struct Host { pub subdomain: Option, pub domain: String, pub tld: String, } impl fmt::Display for Host { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(ref sub) = self.subdomain { write!(f, "{sub}.{}.{}", self.domain, self.tld) } else { write!(f, "{}.{}", self.domain, self.tld) } } } #[derive(Clone, Debug, PartialEq)] pub enum ParseHostError { MissingSeparator, EmptyDomain, EmptyTld, EmptySubdomain, IllegalWhitespace, } impl fmt::Display for ParseHostError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") } } impl std::error::Error for ParseHostError {} impl FromStr for Host { type Err = ParseHostError; fn from_str(s: &str) -> Result { if s.contains(|x: char| x.is_whitespace()) { return Err(ParseHostError::IllegalWhitespace); } if let Some((domain, tld)) = s.rsplit_once('.') { if domain.is_empty() { Err(ParseHostError::EmptyDomain) } else if tld.is_empty() { Err(ParseHostError::EmptyTld) } else if let Some((subdomain, domain)) = domain.rsplit_once('.') { if subdomain.is_empty() { Err(ParseHostError::EmptySubdomain) } else if domain.is_empty() { Err(ParseHostError::EmptyDomain) } else { Ok(Host { subdomain: Some(subdomain.to_string()), domain: domain.to_string(), tld: tld.to_string() }) } } else { Ok(Host { subdomain: None, domain: domain.to_string(), tld: tld.to_string(), }) } } else { Err(ParseHostError::MissingSeparator) } } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_missing_separator() { assert_eq!( "exampledotcom".parse::(), Err(ParseHostError::MissingSeparator) ); } #[test] fn parse_empty_tld() { assert_eq!( "example.".parse::(), Err(ParseHostError::EmptyTld) ); } #[test] fn parse_empty_domain() { assert_eq!( ".com".parse::(), Err(ParseHostError::EmptyDomain) ); assert_eq!( "example..com".parse::(), Err(ParseHostError::EmptyDomain) ); } #[test] fn parse_empty_subdomain() { assert_eq!( ".example.com".parse::(), Err(ParseHostError::EmptySubdomain) ); } #[test] fn parse_illegal_whitespace() { assert_eq!( "exam\tple.com".parse::(), Err(ParseHostError::IllegalWhitespace) ); } #[test] fn parse_host() { assert!("example.com".parse::().is_ok()); } #[test] fn parse_host_with_subdomain() { let host: Host = "mail.example.com".parse().unwrap(); assert_eq!( host.subdomain, Some(String::from("mail")) ); } #[test] fn parse_complex_subdomain() { let host: Host = "mail.sender.example.com".parse().unwrap(); assert_eq!( host.subdomain, Some(String::from("mail.sender")) ); } #[test] fn fmt() { let host = Host { subdomain: None, domain: String::from("example"), tld: String::from("com") }; let host_str = host.to_string(); assert_eq!(host_str.as_str(), "example.com"); } #[test] fn fmt_with_subdomain() { let host = Host { subdomain: Some(String::from("mail")), domain: String::from("example"), tld: String::from("com") }; let host_str = host.to_string(); assert_eq!(host_str.as_str(), "mail.example.com"); } }