kubizone_common/
dn.rs

1use std::fmt::Display;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::{
8    fqdn::FullyQualifiedDomainNameError,
9    segment::{DomainSegment, DomainSegmentError},
10    FullyQualifiedDomainName, PartiallyQualifiedDomainName,
11};
12
13/// Either a [`FullyQualifiedDomainName`] or a [`PartiallyQualifiedDomainName`].
14#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
15#[serde(untagged)]
16pub enum DomainName {
17    /// Domain name is fully qualified.
18    Full(FullyQualifiedDomainName),
19    /// Domain name is partially qualified.
20    Partial(PartiallyQualifiedDomainName),
21}
22
23impl DomainName {
24    /// Returns true if domain is fully qualified.
25    pub fn is_fully_qualified(&self) -> bool {
26        match self {
27            DomainName::Full(_) => true,
28            DomainName::Partial(_) => false,
29        }
30    }
31
32    /// Returns true if domain is only partially qualified.
33    pub fn is_partially_qualified(&self) -> bool {
34        match self {
35            DomainName::Full(_) => false,
36            DomainName::Partial(_) => true,
37        }
38    }
39
40    /// Returns [`None`] if fully qualified, or a reference to the contained partially qualified domain otherwise.
41    pub fn as_partial(&self) -> Option<&PartiallyQualifiedDomainName> {
42        match self {
43            DomainName::Partial(partial) => Some(partial),
44            _ => None,
45        }
46    }
47
48    /// Returns [`None`] if partially qualified, or a reference to the contained fully qualified domain otherwise.
49    pub fn as_full(&self) -> Option<&FullyQualifiedDomainName> {
50        match self {
51            DomainName::Full(full) => Some(full),
52            _ => None,
53        }
54    }
55
56    /// Returns the contained [`DomainSegment`]s as a[`FullyQualifiedDomainName`]
57    pub fn into_fully_qualified(self) -> FullyQualifiedDomainName {
58        match self {
59            DomainName::Full(full) => full,
60            DomainName::Partial(partial) => partial.into_fully_qualified(),
61        }
62    }
63
64    /// Returns the contained [`DomainSegment`]s as a[`PartiallyQualifiedDomainName`]
65    pub fn into_partially_qualified(self) -> PartiallyQualifiedDomainName {
66        match self {
67            DomainName::Full(full) => full.into_partially_qualified(),
68            DomainName::Partial(partial) => partial,
69        }
70    }
71
72    /// Returns the contained [`DomainSegment`]s as a[`FullyQualifiedDomainName`]
73    pub fn to_fully_qualified(&self) -> FullyQualifiedDomainName {
74        match self {
75            DomainName::Full(full) => full.clone(),
76            DomainName::Partial(partial) => partial.to_fully_qualified(),
77        }
78    }
79
80    /// Returns the contained [`DomainSegment`]s as a[`PartiallyQualifiedDomainName`]
81    pub fn to_partially_qualified(&self) -> PartiallyQualifiedDomainName {
82        match self {
83            DomainName::Full(full) => full.to_partially_qualified(),
84            DomainName::Partial(partial) => partial.clone(),
85        }
86    }
87
88    /// Iterates over all [`DomainSegment`]s that make up the domain name.
89    pub fn iter(&self) -> core::slice::Iter<'_, DomainSegment> {
90        match self {
91            DomainName::Full(full) => full.iter(),
92            DomainName::Partial(partial) => partial.iter(),
93        }
94    }
95
96    /// Returns the length of the domain.
97    ///
98    /// Note that fully qualified domain names will include the trailing dot
99    /// in this measurement.
100    #[allow(clippy::len_without_is_empty)]
101    pub fn len(&self) -> usize {
102        match self {
103            DomainName::Full(full) => full.len(),
104            DomainName::Partial(partial) => partial.len(),
105        }
106    }
107}
108
109/// Produced when attempting to construct a [`DomainName`] from
110/// an invalid string.
111#[derive(Error, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
112pub enum DomainNameError {
113    /// Invalid Domain Segment.
114    #[error("segment error: {0}")]
115    SegmentError(DomainSegmentError),
116    /// Wildcards must only appear in the very first segment of a domain.
117    #[error("non-leading wildcard")]
118    NonLeadingWildcard,
119}
120
121impl Default for DomainName {
122    fn default() -> Self {
123        DomainName::Partial(PartiallyQualifiedDomainName::default())
124    }
125}
126
127impl From<PartiallyQualifiedDomainName> for DomainName {
128    fn from(value: PartiallyQualifiedDomainName) -> Self {
129        DomainName::Partial(value)
130    }
131}
132
133impl From<FullyQualifiedDomainName> for DomainName {
134    fn from(value: FullyQualifiedDomainName) -> Self {
135        DomainName::Full(value)
136    }
137}
138
139impl TryFrom<String> for DomainName {
140    type Error = DomainNameError;
141
142    fn try_from(value: String) -> Result<Self, Self::Error> {
143        Self::try_from(value.as_str())
144    }
145}
146
147impl TryFrom<&str> for DomainName {
148    type Error = DomainNameError;
149
150    fn try_from(value: &str) -> Result<Self, Self::Error> {
151        match FullyQualifiedDomainName::try_from(value) {
152            Ok(fqdn) => Ok(DomainName::Full(fqdn)),
153            Err(FullyQualifiedDomainNameError::DomainIsPartiallyQualified) => Ok(
154                DomainName::Partial(PartiallyQualifiedDomainName::try_from(value).unwrap()),
155            ),
156            Err(FullyQualifiedDomainNameError::SegmentError(err)) => {
157                Err(DomainNameError::SegmentError(err))
158            }
159            Err(FullyQualifiedDomainNameError::NonLeadingWildcard) => {
160                Err(DomainNameError::NonLeadingWildcard)
161            }
162        }
163    }
164}
165
166impl Display for DomainName {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        match self {
169            DomainName::Full(full) => full.fmt(f),
170            DomainName::Partial(partial) => partial.fmt(f),
171        }
172    }
173}
174
175impl AsRef<[DomainSegment]> for DomainName {
176    fn as_ref(&self) -> &[DomainSegment] {
177        match self {
178            DomainName::Full(full) => full.as_ref(),
179            DomainName::Partial(partial) => partial.as_ref(),
180        }
181    }
182}
183
184impl PartialEq<PartiallyQualifiedDomainName> for DomainName {
185    fn eq(&self, other: &PartiallyQualifiedDomainName) -> bool {
186        match self {
187            DomainName::Full(_) => false,
188            DomainName::Partial(partial) => partial.eq(other),
189        }
190    }
191}
192
193impl PartialEq<FullyQualifiedDomainName> for DomainName {
194    fn eq(&self, other: &FullyQualifiedDomainName) -> bool {
195        match self {
196            DomainName::Full(full) => full.eq(other),
197            DomainName::Partial(_) => false,
198        }
199    }
200}
201
202impl JsonSchema for DomainName {
203    fn schema_name() -> String {
204        <String as JsonSchema>::schema_name()
205    }
206
207    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
208        <String as JsonSchema>::json_schema(gen)
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use crate::{DomainName, FullyQualifiedDomainName, PartiallyQualifiedDomainName};
215
216    #[test]
217    fn deser() {
218        let fqdn = DomainName::from(FullyQualifiedDomainName::try_from("example.org.").unwrap());
219        let pqdn = DomainName::from(PartiallyQualifiedDomainName::try_from("example.org").unwrap());
220
221        println!("fqdn: {}", serde_yaml::to_string(&fqdn).unwrap());
222        println!("pqdn: {}", serde_yaml::to_string(&pqdn).unwrap());
223
224        assert_eq!(
225            serde_yaml::from_str::<DomainName>(&serde_yaml::to_string(&fqdn).unwrap()).unwrap(),
226            fqdn
227        );
228
229        assert_eq!(
230            serde_yaml::from_str::<DomainName>(&serde_yaml::to_string(&pqdn).unwrap()).unwrap(),
231            pqdn
232        );
233    }
234}