use http::header::{HeaderMap, HOST};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use thiserror::Error;
#[derive(Clone, Copy, Debug, Error)]
pub enum InvalidHostName {
#[error("Host name must not be empty")]
NoLabels,
#[error("Each host name component must not be empty")]
EmptyLabel,
#[error("Each host name component must only contain allowed characters")]
InvalidLabel
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Deserialize, Serialize)]
#[serde(try_from = "&str", into = "String")]
pub struct HostName(String);
impl HostName {
pub fn new(value: &str) -> Result<Self, InvalidHostName> {
let value = value.to_ascii_lowercase();
if value.len() == 0 {
Err(InvalidHostName::NoLabels)
} else {
for part in value.split('.') {
if part.len() == 0 {
return Err(InvalidHostName::EmptyLabel);
} else if !part.chars().all(|x| (x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || (x >= '0' && x <= '9') || x == '-' || x == '_') {
return Err(InvalidHostName::InvalidLabel);
}
}
Ok(Self(value))
}
}
pub fn as_str<'a>(&'a self) -> &'a str {
self.0.as_str()
}
}
impl Display for HostName {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::str::FromStr for HostName {
type Err = InvalidHostName;
fn from_str(value: &str) -> Result<Self, InvalidHostName> {
Self::new(value)
}
}
impl TryFrom<&str> for HostName {
type Error = InvalidHostName;
fn try_from(value: &str) -> Result<Self, InvalidHostName> {
value.parse()
}
}
impl From<HostName> for String {
fn from(value: HostName) -> Self {
value.0
}
}
pub trait HostNameSource {
fn extract_host_name(&self) -> Option<HostName>;
}
impl HostNameSource for HeaderMap {
fn extract_host_name(&self) -> Option<HostName> {
let header_value = self.get(HOST)?.to_str().ok()?;
if let Some((first, _port)) = header_value.rsplit_once(':') {
first.parse().ok()
} else {
header_value.parse().ok()
}
}
}