Skip to main content

bare_types/net/
host.rs

1//! Host type for network programming.
2//!
3//! This module provides a unified `Host` enum that can represent
4//! IP addresses, domain names, or hostnames with automatic parsing.
5//!
6//! # Parsing Order
7//!
8//! When parsing a string, the `Host` type attempts to parse in this order:
9//!
10//! 1. **`IpAddr`** - IPv4 or IPv6 addresses (e.g., "192.168.1.1", "`::1`")
11//! 2. **`DomainName`** - RFC 1035 domain names (e.g., "example.com")
12//! 3. **`Hostname`** - RFC 1123 hostnames (e.g., "localhost")
13//!
14//! # Examples
15//!
16//! ```rust
17//! use bare_types::net::Host;
18//!
19//! // Parse an IP address
20//! let host: Host = "192.168.1.1".parse().unwrap();
21//! assert!(host.is_ipaddr());
22//!
23//! // Parse a domain name (labels can start with digits)
24//! let host: Host = "123.example.com".parse().unwrap();
25//! assert!(host.is_domainname());
26//!
27//! // Create a hostname directly (labels must start with letters)
28//! let hostname = "localhost".parse::<bare_types::net::Hostname>().unwrap();
29//! let host = Host::from_hostname(hostname);
30//! assert!(host.is_hostname());
31//! ```
32
33use core::fmt;
34use core::str::FromStr;
35
36use super::{DomainName, Hostname, IpAddr};
37
38#[cfg(feature = "serde")]
39use serde::{Deserialize, Serialize};
40
41#[cfg(feature = "arbitrary")]
42use arbitrary::Arbitrary;
43
44/// Error type for host parsing.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum HostError {
47    /// Invalid host input
48    InvalidInput,
49}
50
51impl fmt::Display for HostError {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            Self::InvalidInput => write!(f, "invalid host"),
55        }
56    }
57}
58
59#[cfg(feature = "std")]
60impl std::error::Error for HostError {}
61
62/// A network host that can be an IP address, domain name, or hostname.
63///
64/// This enum provides a unified type for representing network hosts,
65/// with automatic parsing that follows a specific priority order.
66///
67/// # Parsing Priority
68///
69/// When parsing from a string, the following order is used:
70///
71/// 1. **`IpAddr`**: IPv4 (e.g., "192.168.1.1") or IPv6 (e.g., "`::1`")
72/// 2. **`DomainName`**: RFC 1035 domain names (labels can start with digits)
73/// 3. **`Hostname`**: RFC 1123 hostnames (labels must start with letters)
74///
75/// # Examples
76///
77/// ```rust
78/// use bare_types::net::Host;
79///
80/// // Create from IP address
81/// let ipaddr = "192.168.1.1".parse::<bare_types::net::IpAddr>().unwrap();
82/// let host = Host::from_ipaddr(ipaddr);
83/// assert!(host.is_ipaddr());
84///
85/// // Create from domain name
86/// let domain = "123.example.com".parse::<bare_types::net::DomainName>().unwrap();
87/// let host = Host::from_domainname(domain);
88/// assert!(host.is_domainname());
89///
90/// // Create from hostname
91/// let hostname = "localhost".parse::<bare_types::net::Hostname>().unwrap();
92/// let host = Host::from_hostname(hostname);
93/// assert!(host.is_hostname());
94///
95/// // Parse with automatic detection
96/// let host: Host = "192.168.1.1".parse().unwrap();
97/// assert!(host.is_ipaddr());
98///
99/// let host: Host = "example.com".parse().unwrap();
100/// assert!(host.is_domainname());
101/// ```
102#[derive(Debug, Clone, PartialEq, Eq, Hash)]
103#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
104pub enum Host {
105    /// An IP address (IPv4 or IPv6)
106    IpAddr(IpAddr),
107    /// A domain name (RFC 1035, labels can start with digits)
108    DomainName(DomainName),
109    /// A hostname (RFC 1123, labels must start with letters)
110    Hostname(Hostname),
111}
112
113#[cfg(feature = "arbitrary")]
114impl<'a> Arbitrary<'a> for Host {
115    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
116        let choice = u8::arbitrary(u)? % 3;
117        match choice {
118            0 => Ok(Self::IpAddr(IpAddr::arbitrary(u)?)),
119            1 => Ok(Self::DomainName(DomainName::arbitrary(u)?)),
120            _ => Ok(Self::Hostname(Hostname::arbitrary(u)?)),
121        }
122    }
123}
124
125impl Host {
126    /// Creates a `Host` from an IP address.
127    ///
128    /// # Examples
129    ///
130    /// ```rust
131    /// use bare_types::net::Host;
132    ///
133    /// let ipaddr = "192.168.1.1".parse::<bare_types::net::IpAddr>()?;
134    /// let host = Host::from_ipaddr(ipaddr);
135    /// assert!(host.is_ipaddr());
136    /// # Ok::<(), bare_types::net::IpAddrError>(())
137    /// ```
138    #[must_use]
139    #[inline]
140    pub const fn from_ipaddr(ipaddr: IpAddr) -> Self {
141        Self::IpAddr(ipaddr)
142    }
143
144    /// Creates a `Host` from a domain name.
145    ///
146    /// # Examples
147    ///
148    /// ```rust
149    /// use bare_types::net::Host;
150    ///
151    /// let domain = "example.com".parse::<bare_types::net::DomainName>()?;
152    /// let host = Host::from_domainname(domain);
153    /// assert!(host.is_domainname());
154    /// # Ok::<(), bare_types::net::DomainNameError>(())
155    /// ```
156    #[must_use]
157    #[inline]
158    pub const fn from_domainname(domain: DomainName) -> Self {
159        Self::DomainName(domain)
160    }
161
162    /// Creates a `Host` from a hostname.
163    ///
164    /// # Examples
165    ///
166    /// ```rust
167    /// use bare_types::net::Host;
168    ///
169    /// let hostname = "example.com".parse::<bare_types::net::Hostname>()?;
170    /// let host = Host::from_hostname(hostname);
171    /// assert!(host.is_hostname());
172    /// # Ok::<(), bare_types::net::HostnameError>(())
173    /// ```
174    #[must_use]
175    #[inline]
176    pub const fn from_hostname(hostname: Hostname) -> Self {
177        Self::Hostname(hostname)
178    }
179
180    /// Returns `true` if this host is an IP address.
181    ///
182    /// # Examples
183    ///
184    /// ```rust
185    /// use bare_types::net::Host;
186    ///
187    /// let host: Host = "192.168.1.1".parse()?;
188    /// assert!(host.is_ipaddr());
189    /// # Ok::<(), bare_types::net::HostError>(())
190    /// ```
191    #[must_use]
192    #[inline]
193    pub const fn is_ipaddr(&self) -> bool {
194        matches!(self, Self::IpAddr(_))
195    }
196
197    /// Returns `true` if this host is a domain name.
198    ///
199    /// # Examples
200    ///
201    /// ```rust
202    /// use bare_types::net::Host;
203    ///
204    /// let host: Host = "123.example.com".parse()?;
205    /// assert!(host.is_domainname());
206    /// # Ok::<(), bare_types::net::HostError>(())
207    /// ```
208    #[must_use]
209    #[inline]
210    pub const fn is_domainname(&self) -> bool {
211        matches!(self, Self::DomainName(_))
212    }
213
214    /// Returns `true` if this host is a hostname.
215    ///
216    /// # Examples
217    ///
218    /// ```rust
219    /// use bare_types::net::Host;
220    ///
221    /// let hostname = "localhost".parse::<bare_types::net::Hostname>()?;
222    /// let host = Host::from_hostname(hostname);
223    /// assert!(host.is_hostname());
224    /// # Ok::<(), bare_types::net::HostnameError>(())
225    /// ```
226    #[must_use]
227    #[inline]
228    pub const fn is_hostname(&self) -> bool {
229        matches!(self, Self::Hostname(_))
230    }
231
232    /// Returns a reference to the IP address if this is an IP address.
233    ///
234    /// # Examples
235    ///
236    /// ```rust
237    /// use bare_types::net::Host;
238    ///
239    /// let host: Host = "192.168.1.1".parse()?;
240    /// assert!(host.as_ipaddr().is_some());
241    /// # Ok::<(), bare_types::net::HostError>(())
242    /// ```
243    #[must_use]
244    #[inline]
245    pub const fn as_ipaddr(&self) -> Option<&IpAddr> {
246        match self {
247            Self::IpAddr(ipaddr) => Some(ipaddr),
248            _ => None,
249        }
250    }
251
252    /// Returns a reference to the domain name if this is a domain name.
253    ///
254    /// # Examples
255    ///
256    /// ```rust
257    /// use bare_types::net::Host;
258    ///
259    /// let host: Host = "123.example.com".parse()?;
260    /// assert!(host.as_domainname().is_some());
261    /// # Ok::<(), bare_types::net::HostError>(())
262    /// ```
263    #[must_use]
264    #[inline]
265    pub const fn as_domainname(&self) -> Option<&DomainName> {
266        match self {
267            Self::DomainName(domain) => Some(domain),
268            _ => None,
269        }
270    }
271
272    /// Returns a reference to the hostname if this is a hostname.
273    ///
274    /// # Examples
275    ///
276    /// ```rust
277    /// use bare_types::net::Host;
278    ///
279    /// let hostname = "localhost".parse::<bare_types::net::Hostname>()?;
280    /// let host = Host::from_hostname(hostname);
281    /// assert!(host.as_hostname().is_some());
282    /// # Ok::<(), bare_types::net::HostnameError>(())
283    /// ```
284    #[must_use]
285    #[inline]
286    pub const fn as_hostname(&self) -> Option<&Hostname> {
287        match self {
288            Self::Hostname(hostname) => Some(hostname),
289            _ => None,
290        }
291    }
292
293    /// Returns `true` if this host represents localhost.
294    ///
295    /// For IP addresses, this checks if it's a loopback address.
296    /// For domain names and hostnames, this checks if it's "localhost".
297    ///
298    /// # Examples
299    ///
300    /// ```rust
301    /// # use std::error::Error;
302    /// # fn main() -> Result<(), Box<dyn Error>> {
303    /// use bare_types::net::Host;
304    ///
305    /// // IPv4 loopback
306    /// let host: Host = "127.0.0.1".parse()?;
307    /// assert!(host.is_localhost());
308    ///
309    /// // IPv6 loopback
310    /// let host: Host = "::1".parse()?;
311    /// assert!(host.is_localhost());
312    ///
313    /// // localhost domain name
314    /// let host: Host = "localhost".parse()?;
315    /// assert!(host.is_localhost());
316    ///
317    /// // Not localhost
318    /// let host: Host = "example.com".parse()?;
319    /// assert!(!host.is_localhost());
320    /// # Ok(())
321    /// # }
322    /// ```
323    #[must_use]
324    #[inline]
325    pub fn is_localhost(&self) -> bool {
326        match self {
327            Self::IpAddr(ip) => ip.is_loopback(),
328            Self::DomainName(domain) => domain.as_str() == "localhost",
329            Self::Hostname(hostname) => hostname.is_localhost(),
330        }
331    }
332
333    /// Parses a string into a `Host` with automatic type detection.
334    ///
335    /// The parsing follows this priority order:
336    /// 1. Try to parse as `IpAddr`
337    /// 2. Try to parse as `DomainName`
338    /// 3. Try to parse as `Hostname`
339    ///
340    /// # Errors
341    ///
342    /// Returns `HostError::InvalidInput` if the string cannot be parsed as
343    /// an IP address, domain name, or hostname.
344    ///
345    /// # Examples
346    ///
347    /// ```rust
348    /// use bare_types::net::Host;
349    ///
350    /// // IP address is parsed first
351    /// let host = Host::parse_str("192.168.1.1")?;
352    /// assert!(host.is_ipaddr());
353    ///
354    /// // Domain name (labels can start with digits)
355    /// let host = Host::parse_str("123.example.com")?;
356    /// assert!(host.is_domainname());
357    ///
358    /// // Domain name is also parsed before hostname for letter-start labels
359    /// let host = Host::parse_str("www.example.com")?;
360    /// assert!(host.is_domainname());
361    /// # Ok::<(), bare_types::net::HostError>(())
362    /// ```
363    pub fn parse_str(s: &str) -> Result<Self, HostError> {
364        if s.is_empty() {
365            return Err(HostError::InvalidInput);
366        }
367
368        // Try parsing as IpAddr first (highest priority)
369        if let Ok(ipaddr) = s.parse::<IpAddr>() {
370            return Ok(Self::IpAddr(ipaddr));
371        }
372
373        // Try parsing as DomainName (allows digit-start labels)
374        if let Ok(domain) = DomainName::new(s) {
375            return Ok(Self::DomainName(domain));
376        }
377
378        // Try parsing as Hostname (requires letter-start labels)
379        if let Ok(hostname) = Hostname::new(s) {
380            return Ok(Self::Hostname(hostname));
381        }
382
383        Err(HostError::InvalidInput)
384    }
385}
386
387impl FromStr for Host {
388    type Err = HostError;
389
390    fn from_str(s: &str) -> Result<Self, Self::Err> {
391        Self::parse_str(s)
392    }
393}
394
395impl fmt::Display for Host {
396    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397        match self {
398            Self::IpAddr(ipaddr) => write!(f, "{ipaddr}"),
399            Self::DomainName(domain) => write!(f, "{domain}"),
400            Self::Hostname(hostname) => write!(f, "{hostname}"),
401        }
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn test_from_ipaddr() {
411        let ipaddr = "192.168.1.1".parse::<IpAddr>().unwrap();
412        let host = Host::from_ipaddr(ipaddr);
413        assert!(host.is_ipaddr());
414        assert!(!host.is_domainname());
415        assert!(!host.is_hostname());
416    }
417
418    #[test]
419    fn test_from_domainname() {
420        let domain = DomainName::new("example.com").unwrap();
421        let host = Host::from_domainname(domain);
422        assert!(!host.is_ipaddr());
423        assert!(host.is_domainname());
424        assert!(!host.is_hostname());
425    }
426
427    #[test]
428    fn test_from_hostname() {
429        let hostname = Hostname::new("example.com").unwrap();
430        let host = Host::from_hostname(hostname);
431        assert!(!host.is_ipaddr());
432        assert!(!host.is_domainname());
433        assert!(host.is_hostname());
434    }
435
436    #[test]
437    fn test_parse_ipv4() {
438        let host: Host = "192.168.1.1".parse().unwrap();
439        assert!(host.is_ipaddr());
440        assert_eq!(format!("{host}"), "192.168.1.1");
441    }
442
443    #[test]
444    fn test_parse_ipv6() {
445        let host: Host = "::1".parse().unwrap();
446        assert!(host.is_ipaddr());
447        assert_eq!(format!("{host}"), "::1");
448    }
449
450    #[test]
451    fn test_parse_domainname_digit_start() {
452        let host: Host = "123.example.com".parse().unwrap();
453        assert!(host.is_domainname());
454        assert_eq!(format!("{host}"), "123.example.com");
455    }
456
457    #[test]
458    fn test_parse_hostname_letter_start() {
459        // Note: With parsing order IpAddr -> DomainName -> Hostname,
460        // letter-start labels are parsed as DomainName (not Hostname)
461        // because DomainName is tried first and also accepts letter-start labels
462        let host: Host = "www.example.com".parse().unwrap();
463        assert!(host.is_domainname());
464        assert_eq!(format!("{host}"), "www.example.com");
465    }
466
467    #[test]
468    fn test_parse_priority_ipaddr_over_domainname() {
469        // "127.0.0.1" could be a valid domain name, but IP address takes priority
470        let host: Host = "127.0.0.1".parse().unwrap();
471        assert!(host.is_ipaddr());
472    }
473
474    #[test]
475    fn test_parse_priority_domainname_over_hostname() {
476        // "123.example.com" is valid as DomainName (digit start)
477        // but invalid as Hostname (must start with letter)
478        let host: Host = "123.example.com".parse().unwrap();
479        assert!(host.is_domainname());
480        assert!(!host.is_hostname());
481    }
482
483    #[test]
484    fn test_parse_str_empty() {
485        assert!(Host::parse_str("").is_err());
486    }
487
488    #[test]
489    fn test_parse_str_invalid() {
490        assert!(Host::parse_str("-invalid").is_err());
491        assert!(Host::parse_str("example..com").is_err());
492    }
493
494    #[test]
495    fn test_as_ipaddr() {
496        let host: Host = "192.168.1.1".parse().unwrap();
497        assert!(host.as_ipaddr().is_some());
498        assert!(host.as_domainname().is_none());
499        assert!(host.as_hostname().is_none());
500    }
501
502    #[test]
503    fn test_as_domainname() {
504        let host: Host = "123.example.com".parse().unwrap();
505        assert!(host.as_ipaddr().is_none());
506        assert!(host.as_domainname().is_some());
507        assert!(host.as_hostname().is_none());
508    }
509
510    #[test]
511    fn test_as_hostname() {
512        // Note: With parsing order IpAddr -> DomainName -> Hostname,
513        // letter-start labels are parsed as DomainName (not Hostname)
514        let host: Host = "www.example.com".parse().unwrap();
515        assert!(host.as_ipaddr().is_none());
516        assert!(host.as_domainname().is_some());
517        assert!(host.as_hostname().is_none());
518    }
519
520    #[test]
521    fn test_equality_ipaddr() {
522        let host1: Host = "192.168.1.1".parse().unwrap();
523        let host2: Host = "192.168.1.1".parse().unwrap();
524        let host3: Host = "192.168.1.2".parse().unwrap();
525
526        assert_eq!(host1, host2);
527        assert_ne!(host1, host3);
528    }
529
530    #[test]
531    fn test_equality_domainname() {
532        let host1: Host = "123.example.com".parse().unwrap();
533        let host2: Host = "123.example.com".parse().unwrap();
534        let host3: Host = "456.example.com".parse().unwrap();
535
536        assert_eq!(host1, host2);
537        assert_ne!(host1, host3);
538    }
539
540    #[test]
541    fn test_equality_hostname() {
542        // Note: With parsing order IpAddr -> DomainName -> Hostname,
543        // letter-start labels are parsed as DomainName (not Hostname)
544        let host1: Host = "www.example.com".parse().unwrap();
545        let host2: Host = "www.example.com".parse().unwrap();
546        let host3: Host = "api.example.com".parse().unwrap();
547
548        assert_eq!(host1, host2);
549        assert_ne!(host1, host3);
550    }
551
552    #[test]
553    fn test_equality_different_types() {
554        let host1: Host = "192.168.1.1".parse().unwrap();
555        let host2: Host = "www.example.com".parse().unwrap();
556
557        assert_ne!(host1, host2);
558    }
559
560    #[test]
561    fn test_clone() {
562        let host: Host = "www.example.com".parse().unwrap();
563        let host2 = host.clone();
564        assert_eq!(host, host2);
565    }
566
567    #[test]
568    fn test_display_ipaddr() {
569        let host: Host = "192.168.1.1".parse().unwrap();
570        assert_eq!(format!("{host}"), "192.168.1.1");
571    }
572
573    #[test]
574    fn test_display_domainname() {
575        let host: Host = "123.example.com".parse().unwrap();
576        assert_eq!(format!("{host}"), "123.example.com");
577    }
578
579    #[test]
580    fn test_display_hostname() {
581        let host: Host = "www.example.com".parse().unwrap();
582        assert_eq!(format!("{host}"), "www.example.com");
583    }
584
585    #[test]
586    fn test_debug() {
587        let host: Host = "www.example.com".parse().unwrap();
588        let debug = format!("{:?}", host);
589        // Note: With parsing order IpAddr -> DomainName -> Hostname,
590        // letter-start labels are parsed as DomainName (not Hostname)
591        assert!(debug.contains("DomainName"));
592    }
593
594    #[test]
595    fn test_hash() {
596        use core::hash::Hash;
597        use core::hash::Hasher;
598
599        #[derive(Default)]
600        struct SimpleHasher(u64);
601
602        impl Hasher for SimpleHasher {
603            fn finish(&self) -> u64 {
604                self.0
605            }
606
607            fn write(&mut self, bytes: &[u8]) {
608                for byte in bytes {
609                    self.0 = self.0.wrapping_mul(31).wrapping_add(*byte as u64);
610                }
611            }
612        }
613
614        let host1: Host = "www.example.com".parse().unwrap();
615        let host2: Host = "www.example.com".parse().unwrap();
616        let host3: Host = "api.example.com".parse().unwrap();
617
618        let mut hasher1 = SimpleHasher::default();
619        let mut hasher2 = SimpleHasher::default();
620        let mut hasher3 = SimpleHasher::default();
621
622        host1.hash(&mut hasher1);
623        host2.hash(&mut hasher2);
624        host3.hash(&mut hasher3);
625
626        assert_eq!(hasher1.finish(), hasher2.finish());
627        assert_ne!(hasher1.finish(), hasher3.finish());
628    }
629
630    #[test]
631    fn test_parse_ipv4_private() {
632        let host: Host = "10.0.0.1".parse().unwrap();
633        assert!(host.is_ipaddr());
634    }
635
636    #[test]
637    fn test_parse_ipv6_loopback() {
638        let host: Host = "::1".parse().unwrap();
639        assert!(host.is_ipaddr());
640    }
641
642    #[test]
643    fn test_parse_ipv6_full() {
644        let host: Host = "2001:0db8:85a3:0000:0000:8a2e:0370:7334".parse().unwrap();
645        assert!(host.is_ipaddr());
646    }
647
648    #[test]
649    fn test_parse_domainname_numeric_label() {
650        let host: Host = "123.456.789".parse().unwrap();
651        assert!(host.is_domainname());
652    }
653
654    #[test]
655    fn test_parse_hostname_multi_label() {
656        // Note: With parsing order IpAddr -> DomainName -> Hostname,
657        // letter-start labels are parsed as DomainName (not Hostname)
658        let host: Host = "api.v1.example.com".parse().unwrap();
659        assert!(host.is_domainname());
660    }
661
662    #[test]
663    fn test_error_display() {
664        let err = HostError::InvalidInput;
665        assert_eq!(format!("{err}"), "invalid host");
666    }
667
668    #[test]
669    fn test_parse_str_method() {
670        let host = Host::parse_str("192.168.1.1").unwrap();
671        assert!(host.is_ipaddr());
672
673        // Note: With parsing order IpAddr -> DomainName -> Hostname,
674        // letter-start labels are parsed as DomainName (not Hostname)
675        let host = Host::parse_str("www.example.com").unwrap();
676        assert!(host.is_domainname());
677    }
678
679    #[test]
680    fn test_case_insensitive_hostname() {
681        // Note: With parsing order IpAddr -> DomainName -> Hostname,
682        // letter-start labels are parsed as DomainName (not Hostname)
683        let host1: Host = "WWW.EXAMPLE.COM".parse().unwrap();
684        let host2: Host = "www.example.com".parse().unwrap();
685        assert_eq!(host1, host2);
686    }
687
688    #[test]
689    fn test_case_insensitive_domainname() {
690        let host1: Host = "123.EXAMPLE.COM".parse().unwrap();
691        let host2: Host = "123.example.com".parse().unwrap();
692        assert_eq!(host1, host2);
693    }
694
695    #[test]
696    fn test_localhost_hostname() {
697        // Note: With parsing order IpAddr -> DomainName -> Hostname,
698        // letter-start labels are parsed as DomainName (not Hostname)
699        let host: Host = "localhost".parse().unwrap();
700        assert!(host.is_domainname());
701    }
702
703    #[test]
704    fn test_is_localhost() {
705        // IPv4 loopback
706        let host: Host = "127.0.0.1".parse().unwrap();
707        assert!(host.is_localhost());
708
709        // IPv6 loopback
710        let host: Host = "::1".parse().unwrap();
711        assert!(host.is_localhost());
712
713        // localhost domain name
714        let host: Host = "localhost".parse().unwrap();
715        assert!(host.is_localhost());
716
717        // Not localhost
718        let host: Host = "example.com".parse().unwrap();
719        assert!(!host.is_localhost());
720
721        let host: Host = "192.168.1.1".parse().unwrap();
722        assert!(!host.is_localhost());
723    }
724
725    #[test]
726    fn test_numeric_only_domainname() {
727        let host: Host = "123".parse().unwrap();
728        assert!(host.is_domainname());
729    }
730
731    #[test]
732    fn test_mixed_alphanumeric_hostname() {
733        // Note: With parsing order IpAddr -> DomainName -> Hostname,
734        // letter-start labels are parsed as DomainName (not Hostname)
735        let host: Host = "api-v1.example.com".parse().unwrap();
736        assert!(host.is_domainname());
737    }
738
739    #[test]
740    fn test_from_hostname_variant() {
741        // Even though parsing prioritizes DomainName, we can still create
742        // Host variants directly from Hostname
743        let hostname = Hostname::new("example.com").unwrap();
744        let host = Host::from_hostname(hostname);
745        assert!(host.is_hostname());
746        assert!(
747            host.as_hostname()
748                .map(|h: &Hostname| h.is_localhost())
749                .unwrap_or(false)
750                == false
751        );
752    }
753}