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