1use std::{
2 fmt::{Debug, Display, Write},
3 ops::Sub,
4};
5
6use schemars::JsonSchema;
7use serde::{de::Error, Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::{
11 segment::{DomainSegment, DomainSegmentError},
12 PartiallyQualifiedDomainName,
13};
14
15#[derive(Error, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
18pub enum FullyQualifiedDomainNameError {
19 #[error("domain is partially qualified")]
22 DomainIsPartiallyQualified,
23 #[error("{0}")]
26 SegmentError(#[from] DomainSegmentError),
27 #[error("non-leading wildcard segment")]
29 NonLeadingWildcard,
30}
31
32#[derive(Default, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
42pub struct FullyQualifiedDomainName(pub(crate) Vec<DomainSegment>);
43
44impl FullyQualifiedDomainName {
45 pub fn iter(&self) -> core::slice::Iter<'_, DomainSegment> {
47 self.0.iter()
48 }
49
50 pub fn is_subdomain_of(&self, parent: &FullyQualifiedDomainName) -> bool {
52 self.0.ends_with(parent.as_ref()) && self != parent
53 }
54
55 #[allow(clippy::len_without_is_empty)]
57 pub fn len(&self) -> usize {
58 self.0.iter().map(|segment| segment.len()).sum::<usize>() + self.0.len()
59 }
60
61 pub fn into_partially_qualified(self) -> PartiallyQualifiedDomainName {
63 PartiallyQualifiedDomainName(self.0)
64 }
65
66 pub fn to_partially_qualified(&self) -> PartiallyQualifiedDomainName {
68 PartiallyQualifiedDomainName(self.0.clone())
69 }
70}
71
72impl FromIterator<DomainSegment> for FullyQualifiedDomainName {
73 fn from_iter<T: IntoIterator<Item = DomainSegment>>(iter: T) -> Self {
74 FullyQualifiedDomainName(iter.into_iter().collect())
75 }
76}
77
78impl<'a> FromIterator<&'a DomainSegment> for FullyQualifiedDomainName {
79 fn from_iter<T: IntoIterator<Item = &'a DomainSegment>>(iter: T) -> Self {
80 FullyQualifiedDomainName(iter.into_iter().cloned().collect())
81 }
82}
83
84impl TryFrom<String> for FullyQualifiedDomainName {
85 type Error = FullyQualifiedDomainNameError;
86
87 fn try_from(value: String) -> Result<Self, Self::Error> {
88 Self::try_from(value.as_str())
89 }
90}
91
92impl TryFrom<&str> for FullyQualifiedDomainName {
93 type Error = FullyQualifiedDomainNameError;
94
95 fn try_from(value: &str) -> Result<Self, Self::Error> {
96 if !value.ends_with('.') {
97 Err(FullyQualifiedDomainNameError::DomainIsPartiallyQualified)
98 } else {
99 let segments: Vec<DomainSegment> = Result::from_iter(
100 value
101 .trim_end_matches('.')
102 .split('.')
103 .map(DomainSegment::try_from),
104 )?;
105
106 if segments.iter().skip(1).any(DomainSegment::is_wildcard) {
107 return Err(FullyQualifiedDomainNameError::NonLeadingWildcard);
108 }
109
110 Ok(FullyQualifiedDomainName(segments))
111 }
112 }
113}
114
115impl Display for FullyQualifiedDomainName {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 for segment in &self.0 {
118 write!(f, "{}", segment)?;
119 f.write_char('.')?;
120 }
121
122 Ok(())
123 }
124}
125
126impl AsRef<[DomainSegment]> for FullyQualifiedDomainName {
127 fn as_ref(&self) -> &[DomainSegment] {
128 self.0.as_ref()
129 }
130}
131
132impl PartialEq<String> for FullyQualifiedDomainName {
133 fn eq(&self, other: &String) -> bool {
134 self.to_string().eq(other)
135 }
136}
137
138impl PartialEq<str> for FullyQualifiedDomainName {
139 fn eq(&self, other: &str) -> bool {
140 self.to_string().eq(other)
141 }
142}
143
144impl JsonSchema for FullyQualifiedDomainName {
145 fn schema_name() -> String {
146 <String as schemars::JsonSchema>::schema_name()
147 }
148
149 fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
150 <String as schemars::JsonSchema>::json_schema(gen)
151 }
152}
153
154impl<'de> Deserialize<'de> for FullyQualifiedDomainName {
155 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
156 where
157 D: serde::Deserializer<'de>,
158 {
159 let value = String::deserialize(deserializer)?;
160
161 Self::try_from(value).map_err(D::Error::custom)
162 }
163}
164
165impl Serialize for FullyQualifiedDomainName {
166 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
167 where
168 S: serde::Serializer,
169 {
170 self.to_string().serialize(serializer)
171 }
172}
173
174impl<'a> Sub for &'a FullyQualifiedDomainName {
175 type Output = Result<PartiallyQualifiedDomainName, &'a FullyQualifiedDomainName>;
176
177 fn sub(self, rhs: Self) -> Self::Output {
178 let mut own_segments = self.0.clone().into_iter().rev();
179 let parent_segments = rhs.0.iter().rev();
180
181 for parent_domain in parent_segments {
182 if !own_segments
183 .next()
184 .is_some_and(|segment| &segment == parent_domain)
185 {
186 return Err(self);
187 }
188 }
189
190 Ok(PartiallyQualifiedDomainName::from_iter(own_segments.rev()))
191 }
192}
193
194impl Sub for FullyQualifiedDomainName {
195 type Output = Result<PartiallyQualifiedDomainName, FullyQualifiedDomainName>;
196
197 fn sub(self, rhs: Self) -> Self::Output {
198 match &self - &rhs {
199 Ok(partial) => Ok(partial),
200 Err(_) => Err(self),
201 }
202 }
203}
204
205#[cfg(test)]
206mod test {
207 use crate::{
208 fqdn::FullyQualifiedDomainNameError, segment::DomainSegment, FullyQualifiedDomainName,
209 PartiallyQualifiedDomainName,
210 };
211
212 #[test]
213 fn construct_fqdn() {
214 assert_eq!(
215 FullyQualifiedDomainName::try_from("example.org."),
216 Ok(FullyQualifiedDomainName::from_iter([
217 DomainSegment::try_from("example").unwrap(),
218 DomainSegment::try_from("org").unwrap()
219 ]))
220 );
221 }
222
223 #[test]
224 fn fqdn_from_pqdn_fails() {
225 assert_eq!(
226 FullyQualifiedDomainName::try_from("example.org"),
227 Err(FullyQualifiedDomainNameError::DomainIsPartiallyQualified)
228 );
229 }
230
231 #[test]
232 fn subtraction() {
233 assert_eq!(
234 FullyQualifiedDomainName::try_from("www.example.org.").unwrap()
235 - FullyQualifiedDomainName::try_from("example.org.").unwrap(),
236 Ok(PartiallyQualifiedDomainName::try_from("www").unwrap())
237 );
238
239 assert_eq!(
240 FullyQualifiedDomainName::try_from("www.example.org.").unwrap()
241 - FullyQualifiedDomainName::try_from("org.").unwrap(),
242 Ok(PartiallyQualifiedDomainName::try_from("www.example").unwrap())
243 );
244
245 assert_eq!(
246 FullyQualifiedDomainName::try_from("www.example.org.").unwrap()
247 - FullyQualifiedDomainName::try_from("test.org.").unwrap(),
248 Err(FullyQualifiedDomainName::try_from("www.example.org.").unwrap())
249 );
250 }
251}