use compose_spec_macros::{DeserializeTryFromString, SerializeDisplay};
use thiserror::Error;
use crate::common::key_impls;
#[derive(
SerializeDisplay, DeserializeTryFromString, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
pub struct Hostname(Box<str>);
impl Hostname {
pub fn new<T>(hostname: T) -> Result<Self, InvalidHostnameError>
where
T: AsRef<str> + Into<Box<str>>,
{
let hostname_str = hostname.as_ref();
for label in hostname_str.split('.') {
if label.is_empty() {
return Err(InvalidHostnameError::LabelEmpty);
}
for char in label.chars() {
if !char.is_ascii_alphanumeric() && char != '-' {
return Err(InvalidHostnameError::Character(char));
}
}
if label.starts_with('-') || label.ends_with('-') {
return Err(InvalidHostnameError::LabelStartEnd);
}
}
Ok(Self(hostname.into()))
}
}
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvalidHostnameError {
#[error("hostname label was empty")]
LabelEmpty,
#[error(
"invalid hostname character `{0}`, hostnames can only contain ASCII letters (a-z, A-Z), \
digits (0-9), dots (.), and dashes (-)"
)]
Character(char),
#[error("hostname labels cannot start or end with dashes (-)")]
LabelStartEnd,
}
key_impls!(Hostname => InvalidHostnameError);
#[cfg(test)]
mod tests {
use pomsky_macro::pomsky;
use proptest::proptest;
use super::*;
const HOSTNAME: &str = pomsky! {
let end = [ascii_alnum];
let middle = [ascii_alnum '-']*;
let label = end (middle end)?;
label ('.' label)*
};
proptest! {
#[test]
fn no_panic(string: String) {
let _ = Hostname::new(string);
}
#[test]
fn valid(string in HOSTNAME) {
Hostname::new(string)?;
}
}
}