compose_spec/service/
hostname.rs1use compose_spec_macros::{DeserializeTryFromString, SerializeDisplay};
4use thiserror::Error;
5
6use crate::common::key_impls;
7
8#[derive(
14 SerializeDisplay, DeserializeTryFromString, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
15)]
16pub struct Hostname(Box<str>);
17
18impl Hostname {
19 pub fn new<T>(hostname: T) -> Result<Self, InvalidHostnameError>
27 where
28 T: AsRef<str> + Into<Box<str>>,
29 {
30 let hostname_str = hostname.as_ref();
31
32 for label in hostname_str.split('.') {
33 if label.is_empty() {
34 return Err(InvalidHostnameError::LabelEmpty);
35 }
36 for char in label.chars() {
37 if !char.is_ascii_alphanumeric() && char != '-' {
38 return Err(InvalidHostnameError::Character(char));
39 }
40 }
41 if label.starts_with('-') || label.ends_with('-') {
42 return Err(InvalidHostnameError::LabelStartEnd);
43 }
44 }
45
46 Ok(Self(hostname.into()))
47 }
48}
49
50#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
52pub enum InvalidHostnameError {
53 #[error("hostname label was empty")]
55 LabelEmpty,
56
57 #[error(
59 "invalid hostname character `{0}`, hostnames can only contain ASCII letters (a-z, A-Z), \
60 digits (0-9), dots (.), and dashes (-)"
61 )]
62 Character(char),
63
64 #[error("hostname labels cannot start or end with dashes (-)")]
66 LabelStartEnd,
67}
68
69key_impls!(Hostname => InvalidHostnameError);
70
71#[cfg(test)]
72mod tests {
73 use pomsky_macro::pomsky;
74 use proptest::proptest;
75
76 use super::*;
77
78 const HOSTNAME: &str = pomsky! {
79 let end = [ascii_alnum];
80 let middle = [ascii_alnum '-']*;
81 let label = end (middle end)?;
82
83 label ('.' label)*
84 };
85
86 proptest! {
87 #[test]
88 fn no_panic(string: String) {
89 let _ = Hostname::new(string);
90 }
91
92 #[test]
93 fn valid(string in HOSTNAME) {
94 Hostname::new(string)?;
95 }
96 }
97}