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#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
15#[serde(untagged)]
16pub enum DomainName {
17 Full(FullyQualifiedDomainName),
19 Partial(PartiallyQualifiedDomainName),
21}
22
23impl DomainName {
24 pub fn is_fully_qualified(&self) -> bool {
26 match self {
27 DomainName::Full(_) => true,
28 DomainName::Partial(_) => false,
29 }
30 }
31
32 pub fn is_partially_qualified(&self) -> bool {
34 match self {
35 DomainName::Full(_) => false,
36 DomainName::Partial(_) => true,
37 }
38 }
39
40 pub fn as_partial(&self) -> Option<&PartiallyQualifiedDomainName> {
42 match self {
43 DomainName::Partial(partial) => Some(partial),
44 _ => None,
45 }
46 }
47
48 pub fn as_full(&self) -> Option<&FullyQualifiedDomainName> {
50 match self {
51 DomainName::Full(full) => Some(full),
52 _ => None,
53 }
54 }
55
56 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 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 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 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 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 #[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#[derive(Error, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
112pub enum DomainNameError {
113 #[error("segment error: {0}")]
115 SegmentError(DomainSegmentError),
116 #[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}