1use crate::error::ParseError;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::str::FromStr;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct Fqdn(String);
11
12impl Fqdn {
13 pub fn new(domain: impl Into<String>) -> Result<Self, ParseError> {
18 let domain = domain.into();
19
20 if domain.is_empty() {
22 return Err(ParseError::InvalidFqdn("empty domain".to_string()));
23 }
24
25 let domain = domain.trim_end_matches('.');
27
28 for label in domain.split('.') {
30 if label.is_empty() {
31 return Err(ParseError::InvalidFqdn("empty label".to_string()));
32 }
33 if label.len() > 63 {
34 return Err(ParseError::InvalidFqdn("label too long".to_string()));
35 }
36 if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
37 return Err(ParseError::InvalidFqdn(format!(
38 "invalid character in label: {label}"
39 )));
40 }
41 if label.starts_with('-') || label.ends_with('-') {
42 return Err(ParseError::InvalidFqdn(
43 "label cannot start or end with hyphen".to_string(),
44 ));
45 }
46 }
47
48 Ok(Self(domain.to_lowercase()))
49 }
50
51 pub fn as_str(&self) -> &str {
53 &self.0
54 }
55
56 pub fn ans_badge_name(&self) -> String {
58 format!("_ans-badge.{}", self.0)
59 }
60
61 pub fn ra_badge_name(&self) -> String {
63 format!("_ra-badge.{}", self.0)
64 }
65
66 pub fn tlsa_name(&self, port: u16) -> String {
68 format!("_{port}._tcp.{}", self.0)
69 }
70}
71
72impl fmt::Display for Fqdn {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 write!(f, "{}", self.0)
75 }
76}
77
78impl FromStr for Fqdn {
79 type Err = ParseError;
80
81 fn from_str(s: &str) -> Result<Self, Self::Err> {
82 Self::new(s)
83 }
84}
85
86impl AsRef<str> for Fqdn {
87 fn as_ref(&self) -> &str {
88 &self.0
89 }
90}
91
92impl TryFrom<&str> for Fqdn {
93 type Error = ParseError;
94
95 fn try_from(s: &str) -> Result<Self, Self::Error> {
96 Self::new(s)
97 }
98}
99
100impl TryFrom<String> for Fqdn {
101 type Error = ParseError;
102
103 fn try_from(s: String) -> Result<Self, Self::Error> {
104 Self::new(s)
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
110pub struct Version {
111 major: u32,
112 minor: u32,
113 patch: u32,
114}
115
116impl Version {
117 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
119 Self {
120 major,
121 minor,
122 patch,
123 }
124 }
125
126 pub const fn major(&self) -> u32 {
128 self.major
129 }
130
131 pub const fn minor(&self) -> u32 {
133 self.minor
134 }
135
136 pub const fn patch(&self) -> u32 {
138 self.patch
139 }
140
141 pub fn parse(s: &str) -> Result<Self, ParseError> {
143 let s = s.strip_prefix('v').unwrap_or(s);
144 let parts: Vec<&str> = s.split('.').collect();
145
146 if parts.len() != 3 {
147 return Err(ParseError::InvalidVersion(format!(
148 "expected 3 parts, got {}: {}",
149 parts.len(),
150 s
151 )));
152 }
153
154 let major = parts[0].parse().map_err(|_| {
155 ParseError::InvalidVersion(format!("invalid major version: {}", parts[0]))
156 })?;
157 let minor = parts[1].parse().map_err(|_| {
158 ParseError::InvalidVersion(format!("invalid minor version: {}", parts[1]))
159 })?;
160 let patch = parts[2].parse().map_err(|_| {
161 ParseError::InvalidVersion(format!("invalid patch version: {}", parts[2]))
162 })?;
163
164 Ok(Self {
165 major,
166 minor,
167 patch,
168 })
169 }
170}
171
172impl fmt::Display for Version {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 write!(f, "v{}.{}.{}", self.major, self.minor, self.patch)
175 }
176}
177
178impl FromStr for Version {
179 type Err = ParseError;
180
181 fn from_str(s: &str) -> Result<Self, Self::Err> {
182 Self::parse(s)
183 }
184}
185
186impl TryFrom<&str> for Version {
187 type Error = ParseError;
188
189 fn try_from(s: &str) -> Result<Self, Self::Error> {
190 Self::parse(s)
191 }
192}
193
194impl TryFrom<String> for Version {
195 type Error = ParseError;
196
197 fn try_from(s: String) -> Result<Self, Self::Error> {
198 Self::parse(&s)
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Hash)]
204pub struct AnsName {
205 version: Version,
206 fqdn: Fqdn,
207}
208
209impl AnsName {
210 pub fn version(&self) -> &Version {
212 &self.version
213 }
214
215 pub fn fqdn(&self) -> &Fqdn {
217 &self.fqdn
218 }
219
220 pub fn parse(uri: &str) -> Result<Self, ParseError> {
224 const PREFIX: &str = "ans://";
225
226 if !uri.starts_with(PREFIX) {
227 return Err(ParseError::InvalidAnsName(format!(
228 "ANS name must start with '{PREFIX}': {uri}"
229 )));
230 }
231
232 let rest = &uri[PREFIX.len()..];
233
234 if !rest.starts_with('v') {
237 return Err(ParseError::InvalidAnsName(format!(
238 "ANS name version must start with 'v': {uri}"
239 )));
240 }
241
242 let parts: Vec<&str> = rest.splitn(4, '.').collect();
243 if parts.len() < 4 {
244 return Err(ParseError::InvalidAnsName(format!(
245 "ANS name must have format 'ans://vX.Y.Z.fqdn', got: {uri}"
246 )));
247 }
248
249 let version_str = format!("{}.{}.{}", parts[0], parts[1], parts[2]);
251 let version = Version::parse(&version_str)?;
252
253 let fqdn = Fqdn::new(parts[3])?;
255
256 Ok(Self { version, fqdn })
257 }
258}
259
260impl fmt::Display for AnsName {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 write!(f, "ans://{}.{}", self.version, self.fqdn)
263 }
264}
265
266impl FromStr for AnsName {
267 type Err = ParseError;
268
269 fn from_str(s: &str) -> Result<Self, Self::Err> {
270 Self::parse(s)
271 }
272}
273
274impl Serialize for AnsName {
275 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
276 serializer.serialize_str(&self.to_string())
277 }
278}
279
280impl<'de> Deserialize<'de> for AnsName {
281 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
282 let s = String::deserialize(deserializer)?;
283 Self::parse(&s).map_err(serde::de::Error::custom)
284 }
285}
286
287impl TryFrom<&str> for AnsName {
288 type Error = ParseError;
289
290 fn try_from(s: &str) -> Result<Self, Self::Error> {
291 Self::parse(s)
292 }
293}
294
295impl TryFrom<String> for AnsName {
296 type Error = ParseError;
297
298 fn try_from(s: String) -> Result<Self, Self::Error> {
299 Self::parse(&s)
300 }
301}
302
303#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 mod fqdn_tests {
309 use super::*;
310
311 #[test]
312 fn test_valid_fqdn() {
313 let fqdn = Fqdn::new("agent.example.com").unwrap();
314 assert_eq!(fqdn.as_str(), "agent.example.com");
315 }
316
317 #[test]
318 fn test_fqdn_with_trailing_dot() {
319 let fqdn = Fqdn::new("agent.example.com.").unwrap();
320 assert_eq!(fqdn.as_str(), "agent.example.com");
321 }
322
323 #[test]
324 fn test_fqdn_lowercased() {
325 let fqdn = Fqdn::new("Agent.Example.COM").unwrap();
326 assert_eq!(fqdn.as_str(), "agent.example.com");
327 }
328
329 #[test]
330 fn test_ans_badge_name() {
331 let fqdn = Fqdn::new("agent.example.com").unwrap();
332 assert_eq!(fqdn.ans_badge_name(), "_ans-badge.agent.example.com");
333 }
334
335 #[test]
336 fn test_ra_badge_name() {
337 let fqdn = Fqdn::new("agent.example.com").unwrap();
338 assert_eq!(fqdn.ra_badge_name(), "_ra-badge.agent.example.com");
339 }
340
341 #[test]
342 fn test_tlsa_name() {
343 let fqdn = Fqdn::new("agent.example.com").unwrap();
344 assert_eq!(fqdn.tlsa_name(443), "_443._tcp.agent.example.com");
345 }
346
347 #[test]
348 fn test_empty_fqdn() {
349 assert!(Fqdn::new("").is_err());
350 }
351
352 #[test]
353 fn test_fqdn_with_invalid_chars() {
354 assert!(Fqdn::new("agent_test.example.com").is_err());
355 }
356 }
357
358 mod version_tests {
359 use super::*;
360
361 #[test]
362 fn test_parse_with_v_prefix() {
363 let v = Version::parse("v1.2.3").unwrap();
364 assert_eq!(v.major(), 1);
365 assert_eq!(v.minor(), 2);
366 assert_eq!(v.patch(), 3);
367 }
368
369 #[test]
370 fn test_parse_without_v_prefix() {
371 let v = Version::parse("1.2.3").unwrap();
372 assert_eq!(v.major(), 1);
373 assert_eq!(v.minor(), 2);
374 assert_eq!(v.patch(), 3);
375 }
376
377 #[test]
378 fn test_version_display() {
379 let v = Version::new(1, 2, 3);
380 assert_eq!(v.to_string(), "v1.2.3");
381 }
382
383 #[test]
384 fn test_version_ordering() {
385 let v1 = Version::new(1, 0, 0);
386 let v2 = Version::new(1, 0, 1);
387 let v3 = Version::new(1, 1, 0);
388 let v4 = Version::new(2, 0, 0);
389 assert!(v1 < v2);
390 assert!(v2 < v3);
391 assert!(v3 < v4);
392 }
393
394 #[test]
395 fn test_invalid_version() {
396 assert!(Version::parse("1.2").is_err());
397 assert!(Version::parse("1.2.3.4").is_err());
398 assert!(Version::parse("a.b.c").is_err());
399 }
400 }
401
402 mod fqdn_extra_tests {
403 use super::*;
404
405 #[test]
406 fn test_fqdn_single_label() {
407 let fqdn = Fqdn::new("localhost").unwrap();
408 assert_eq!(fqdn.as_str(), "localhost");
409 }
410
411 #[test]
412 fn test_fqdn_label_too_long() {
413 let long_label = "a".repeat(64);
414 assert!(Fqdn::new(&long_label).is_err());
415 }
416
417 #[test]
418 fn test_fqdn_leading_hyphen() {
419 assert!(Fqdn::new("-example.com").is_err());
420 }
421
422 #[test]
423 fn test_fqdn_trailing_hyphen() {
424 assert!(Fqdn::new("example-.com").is_err());
425 }
426
427 #[test]
428 fn test_fqdn_double_dots() {
429 assert!(Fqdn::new("agent..example.com").is_err());
430 }
431
432 #[test]
433 fn test_fqdn_display() {
434 let fqdn = Fqdn::new("agent.example.com").unwrap();
435 assert_eq!(format!("{fqdn}"), "agent.example.com");
436 }
437
438 #[test]
439 fn test_fqdn_as_ref() {
440 let fqdn = Fqdn::new("agent.example.com").unwrap();
441 let s: &str = fqdn.as_ref();
442 assert_eq!(s, "agent.example.com");
443 }
444
445 #[test]
446 fn test_fqdn_try_from_str() {
447 let fqdn = Fqdn::try_from("agent.example.com").unwrap();
448 assert_eq!(fqdn.as_str(), "agent.example.com");
449 }
450
451 #[test]
452 fn test_fqdn_try_from_string() {
453 let fqdn = Fqdn::try_from("agent.example.com".to_string()).unwrap();
454 assert_eq!(fqdn.as_str(), "agent.example.com");
455 }
456
457 #[test]
458 fn test_fqdn_from_str() {
459 let fqdn: Fqdn = "agent.example.com".parse().unwrap();
460 assert_eq!(fqdn.as_str(), "agent.example.com");
461 }
462 }
463
464 mod version_extra_tests {
465 use super::*;
466
467 #[test]
468 fn test_version_try_from_str() {
469 let v = Version::try_from("v1.2.3").unwrap();
470 assert_eq!(v, Version::new(1, 2, 3));
471 }
472
473 #[test]
474 fn test_version_try_from_string() {
475 let v = Version::try_from("1.2.3".to_string()).unwrap();
476 assert_eq!(v, Version::new(1, 2, 3));
477 }
478
479 #[test]
480 fn test_version_from_str() {
481 let v: Version = "v1.0.0".parse().unwrap();
482 assert_eq!(v, Version::new(1, 0, 0));
483 }
484
485 #[test]
486 fn test_version_hash_equality() {
487 use std::collections::HashSet;
488 let mut set = HashSet::new();
489 set.insert(Version::new(1, 0, 0));
490 assert!(set.contains(&Version::new(1, 0, 0)));
491 assert!(!set.contains(&Version::new(1, 0, 1)));
492 }
493 }
494
495 mod ans_name_tests {
496 use super::*;
497
498 #[test]
499 fn test_parse_ans_name() {
500 let name = AnsName::parse("ans://v1.0.0.agent.example.com").unwrap();
501 assert_eq!(name.version, Version::new(1, 0, 0));
502 assert_eq!(name.fqdn.as_str(), "agent.example.com");
503 }
504
505 #[test]
506 fn test_parse_ans_name_complex_fqdn() {
507 let name = AnsName::parse("ans://v2.1.3.agent.example.com").unwrap();
508 assert_eq!(name.version, Version::new(2, 1, 3));
509 assert_eq!(name.fqdn.as_str(), "agent.example.com");
510 }
511
512 #[test]
513 fn test_invalid_ans_name_no_prefix() {
514 assert!(AnsName::parse("v1.0.0.agent.example.com").is_err());
515 }
516
517 #[test]
518 fn test_invalid_ans_name_no_version() {
519 assert!(AnsName::parse("ans://agent.example.com").is_err());
520 }
521
522 #[test]
523 fn test_ans_name_display() {
524 let name = AnsName::parse("ans://v1.0.0.agent.example.com").unwrap();
525 assert_eq!(format!("{name}"), "ans://v1.0.0.agent.example.com");
526 }
527
528 #[test]
529 fn test_ans_name_serde_roundtrip() {
530 let name = AnsName::parse("ans://v1.0.0.agent.example.com").unwrap();
531 let json = serde_json::to_string(&name).unwrap();
532 let deserialized: AnsName = serde_json::from_str(&json).unwrap();
533 assert_eq!(name, deserialized);
534 }
535
536 #[test]
537 fn test_ans_name_serde_invalid() {
538 let result = serde_json::from_str::<AnsName>(r#""not-an-ans-name""#);
539 assert!(result.is_err());
540 }
541
542 #[test]
543 fn test_ans_name_try_from_str() {
544 let name = AnsName::try_from("ans://v1.0.0.agent.example.com").unwrap();
545 assert_eq!(name.version(), &Version::new(1, 0, 0));
546 }
547
548 #[test]
549 fn test_ans_name_try_from_string() {
550 let name = AnsName::try_from("ans://v1.0.0.agent.example.com".to_string()).unwrap();
551 assert_eq!(name.fqdn().as_str(), "agent.example.com");
552 }
553
554 #[test]
555 fn test_ans_name_from_str() {
556 let name: AnsName = "ans://v1.0.0.agent.example.com".parse().unwrap();
557 assert_eq!(name.version(), &Version::new(1, 0, 0));
558 }
559
560 #[test]
561 fn test_ans_name_accessors() {
562 let name = AnsName::parse("ans://v2.1.3.agent.example.com").unwrap();
563 assert_eq!(name.version(), &Version::new(2, 1, 3));
564 assert_eq!(name.fqdn().as_str(), "agent.example.com");
565 }
566
567 #[test]
568 fn test_ans_name_no_v_prefix_error() {
569 let result = AnsName::parse("ans://1.0.0.agent.example.com");
570 assert!(result.is_err());
571 }
572 }
573}