use std::{fmt::Display, sync::LazyLock};
use regex::Regex;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use utoipa::ToSchema;
pub trait Id {
fn from_usize(val: usize) -> Self;
fn as_usize(&self) -> usize;
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, ToSchema, Serialize)]
#[serde(transparent)]
pub struct Hostname(String);
#[derive(Debug, Error)]
pub enum HostnameError {
#[error("Hostname is empty")]
Empty,
#[error("Hostname is too long: {0} characters (maximum is 253)")]
TooLong(usize),
#[error("Invalid hostname: {0}")]
Invalid(String),
}
pub const HOSTNAME_REGEX: &str = r"^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])(\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$";
impl Hostname {
pub fn new(hostname: String) -> Result<Self, HostnameError> {
Self::validate(&hostname)?;
Ok(Self(hostname))
}
fn validate(hostname: &str) -> Result<(), HostnameError> {
if hostname.len() > 253 {
return Err(HostnameError::TooLong(hostname.len()));
}
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(HOSTNAME_REGEX).expect("valid regex"));
if !RE.is_match(hostname) {
return Err(HostnameError::Invalid(hostname.to_string()));
}
Ok(())
}
}
impl Display for Hostname {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<Hostname> for String {
fn from(value: Hostname) -> Self {
value.0
}
}
impl TryFrom<String> for Hostname {
type Error = HostnameError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<&str> for Hostname {
type Error = HostnameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value.to_string())
}
}
impl<'de> Deserialize<'de> for Hostname {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Hostname::new(s).map_err(serde::de::Error::custom)
}
}