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, 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 bare_types::net::Host;
302    ///
303    /// // IPv4 loopback
304    /// let host: Host = "127.0.0.1".parse()?;
305    /// assert!(host.is_localhost());
306    ///
307    /// // IPv6 loopback
308    /// let host: Host = "::1".parse()?;
309    /// assert!(host.is_localhost());
310    ///
311    /// // localhost domain name
312    /// let host: Host = "localhost".parse()?;
313    /// assert!(host.is_localhost());
314    ///
315    /// // Not localhost
316    /// let host: Host = "example.com".parse()?;
317    /// assert!(!host.is_localhost());
318    /// # Ok::<(), Box<dyn std::error::Error>>(())
319    /// ```
320    #[must_use]
321    pub fn is_localhost(&self) -> bool {
322        match self {
323            Self::IpAddr(ip) => ip.is_loopback(),
324            Self::DomainName(domain) => domain.as_str() == "localhost",
325            Self::Hostname(hostname) => hostname.is_localhost(),
326        }
327    }
328
329    /// Parses a string into a `Host` with automatic type detection.
330    ///
331    /// The parsing follows this priority order:
332    /// 1. Try to parse as `IpAddr`
333    /// 2. Try to parse as `DomainName`
334    /// 3. Try to parse as `Hostname`
335    ///
336    /// # Errors
337    ///
338    /// Returns `HostError::InvalidInput` if the string cannot be parsed as
339    /// an IP address, domain name, or hostname.
340    ///
341    /// # Examples
342    ///
343    /// ```rust
344    /// use bare_types::net::Host;
345    ///
346    /// // IP address is parsed first
347    /// let host = Host::parse_str("192.168.1.1")?;
348    /// assert!(host.is_ipaddr());
349    ///
350    /// // Domain name (labels can start with digits)
351    /// let host = Host::parse_str("123.example.com")?;
352    /// assert!(host.is_domainname());
353    ///
354    /// // Domain name is also parsed before hostname for letter-start labels
355    /// let host = Host::parse_str("www.example.com")?;
356    /// assert!(host.is_domainname());
357    /// # Ok::<(), bare_types::net::HostError>(())
358    /// ```
359    pub fn parse_str(s: &str) -> Result<Self, HostError> {
360        if s.is_empty() {
361            return Err(HostError::InvalidInput);
362        }
363
364        // Try parsing as IpAddr first (highest priority)
365        if let Ok(ipaddr) = s.parse::<IpAddr>() {
366            return Ok(Self::IpAddr(ipaddr));
367        }
368
369        // Try parsing as DomainName (allows digit-start labels)
370        if let Ok(domain) = DomainName::new(s) {
371            return Ok(Self::DomainName(domain));
372        }
373
374        // Try parsing as Hostname (requires letter-start labels)
375        if let Ok(hostname) = Hostname::new(s) {
376            return Ok(Self::Hostname(hostname));
377        }
378
379        Err(HostError::InvalidInput)
380    }
381}
382
383impl FromStr for Host {
384    type Err = HostError;
385
386    fn from_str(s: &str) -> Result<Self, Self::Err> {
387        Self::parse_str(s)
388    }
389}
390
391impl fmt::Display for Host {
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393        match self {
394            Self::IpAddr(ipaddr) => write!(f, "{ipaddr}"),
395            Self::DomainName(domain) => write!(f, "{domain}"),
396            Self::Hostname(hostname) => write!(f, "{hostname}"),
397        }
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn test_from_ipaddr() {
407        let ipaddr = "192.168.1.1".parse::<IpAddr>().unwrap();
408        let host = Host::from_ipaddr(ipaddr);
409        assert!(host.is_ipaddr());
410        assert!(!host.is_domainname());
411        assert!(!host.is_hostname());
412    }
413
414    #[test]
415    fn test_from_domainname() {
416        let domain = DomainName::new("example.com").unwrap();
417        let host = Host::from_domainname(domain);
418        assert!(!host.is_ipaddr());
419        assert!(host.is_domainname());
420        assert!(!host.is_hostname());
421    }
422
423    #[test]
424    fn test_from_hostname() {
425        let hostname = Hostname::new("example.com").unwrap();
426        let host = Host::from_hostname(hostname);
427        assert!(!host.is_ipaddr());
428        assert!(!host.is_domainname());
429        assert!(host.is_hostname());
430    }
431
432    #[test]
433    fn test_parse_ipv4() {
434        let host: Host = "192.168.1.1".parse().unwrap();
435        assert!(host.is_ipaddr());
436        assert_eq!(format!("{host}"), "192.168.1.1");
437    }
438
439    #[test]
440    fn test_parse_ipv6() {
441        let host: Host = "::1".parse().unwrap();
442        assert!(host.is_ipaddr());
443        assert_eq!(format!("{host}"), "::1");
444    }
445
446    #[test]
447    fn test_parse_domainname_digit_start() {
448        let host: Host = "123.example.com".parse().unwrap();
449        assert!(host.is_domainname());
450        assert_eq!(format!("{host}"), "123.example.com");
451    }
452
453    #[test]
454    fn test_parse_hostname_letter_start() {
455        // Note: With parsing order IpAddr -> DomainName -> Hostname,
456        // letter-start labels are parsed as DomainName (not Hostname)
457        // because DomainName is tried first and also accepts letter-start labels
458        let host: Host = "www.example.com".parse().unwrap();
459        assert!(host.is_domainname());
460        assert_eq!(format!("{host}"), "www.example.com");
461    }
462
463    #[test]
464    fn test_parse_priority_ipaddr_over_domainname() {
465        // "127.0.0.1" could be a valid domain name, but IP address takes priority
466        let host: Host = "127.0.0.1".parse().unwrap();
467        assert!(host.is_ipaddr());
468    }
469
470    #[test]
471    fn test_parse_priority_domainname_over_hostname() {
472        // "123.example.com" is valid as DomainName (digit start)
473        // but invalid as Hostname (must start with letter)
474        let host: Host = "123.example.com".parse().unwrap();
475        assert!(host.is_domainname());
476        assert!(!host.is_hostname());
477    }
478
479    #[test]
480    fn test_parse_str_empty() {
481        assert!(Host::parse_str("").is_err());
482    }
483
484    #[test]
485    fn test_parse_str_invalid() {
486        assert!(Host::parse_str("-invalid").is_err());
487        assert!(Host::parse_str("example..com").is_err());
488    }
489
490    #[test]
491    fn test_as_ipaddr() {
492        let host: Host = "192.168.1.1".parse().unwrap();
493        assert!(host.as_ipaddr().is_some());
494        assert!(host.as_domainname().is_none());
495        assert!(host.as_hostname().is_none());
496    }
497
498    #[test]
499    fn test_as_domainname() {
500        let host: Host = "123.example.com".parse().unwrap();
501        assert!(host.as_ipaddr().is_none());
502        assert!(host.as_domainname().is_some());
503        assert!(host.as_hostname().is_none());
504    }
505
506    #[test]
507    fn test_as_hostname() {
508        // Note: With parsing order IpAddr -> DomainName -> Hostname,
509        // letter-start labels are parsed as DomainName (not Hostname)
510        let host: Host = "www.example.com".parse().unwrap();
511        assert!(host.as_ipaddr().is_none());
512        assert!(host.as_domainname().is_some());
513        assert!(host.as_hostname().is_none());
514    }
515
516    #[test]
517    fn test_equality_ipaddr() {
518        let host1: Host = "192.168.1.1".parse().unwrap();
519        let host2: Host = "192.168.1.1".parse().unwrap();
520        let host3: Host = "192.168.1.2".parse().unwrap();
521
522        assert_eq!(host1, host2);
523        assert_ne!(host1, host3);
524    }
525
526    #[test]
527    fn test_equality_domainname() {
528        let host1: Host = "123.example.com".parse().unwrap();
529        let host2: Host = "123.example.com".parse().unwrap();
530        let host3: Host = "456.example.com".parse().unwrap();
531
532        assert_eq!(host1, host2);
533        assert_ne!(host1, host3);
534    }
535
536    #[test]
537    fn test_equality_hostname() {
538        // Note: With parsing order IpAddr -> DomainName -> Hostname,
539        // letter-start labels are parsed as DomainName (not Hostname)
540        let host1: Host = "www.example.com".parse().unwrap();
541        let host2: Host = "www.example.com".parse().unwrap();
542        let host3: Host = "api.example.com".parse().unwrap();
543
544        assert_eq!(host1, host2);
545        assert_ne!(host1, host3);
546    }
547
548    #[test]
549    fn test_equality_different_types() {
550        let host1: Host = "192.168.1.1".parse().unwrap();
551        let host2: Host = "www.example.com".parse().unwrap();
552
553        assert_ne!(host1, host2);
554    }
555
556    #[test]
557    fn test_clone() {
558        let host: Host = "www.example.com".parse().unwrap();
559        let host2 = host.clone();
560        assert_eq!(host, host2);
561    }
562
563    #[test]
564    fn test_display_ipaddr() {
565        let host: Host = "192.168.1.1".parse().unwrap();
566        assert_eq!(format!("{host}"), "192.168.1.1");
567    }
568
569    #[test]
570    fn test_display_domainname() {
571        let host: Host = "123.example.com".parse().unwrap();
572        assert_eq!(format!("{host}"), "123.example.com");
573    }
574
575    #[test]
576    fn test_display_hostname() {
577        let host: Host = "www.example.com".parse().unwrap();
578        assert_eq!(format!("{host}"), "www.example.com");
579    }
580
581    #[test]
582    fn test_debug() {
583        let host: Host = "www.example.com".parse().unwrap();
584        let debug = format!("{:?}", host);
585        // Note: With parsing order IpAddr -> DomainName -> Hostname,
586        // letter-start labels are parsed as DomainName (not Hostname)
587        assert!(debug.contains("DomainName"));
588    }
589
590    #[test]
591    fn test_hash() {
592        use core::hash::Hash;
593        use core::hash::Hasher;
594
595        #[derive(Default)]
596        struct SimpleHasher(u64);
597
598        impl Hasher for SimpleHasher {
599            fn finish(&self) -> u64 {
600                self.0
601            }
602
603            fn write(&mut self, bytes: &[u8]) {
604                for byte in bytes {
605                    self.0 = self.0.wrapping_mul(31).wrapping_add(*byte as u64);
606                }
607            }
608        }
609
610        let host1: Host = "www.example.com".parse().unwrap();
611        let host2: Host = "www.example.com".parse().unwrap();
612        let host3: Host = "api.example.com".parse().unwrap();
613
614        let mut hasher1 = SimpleHasher::default();
615        let mut hasher2 = SimpleHasher::default();
616        let mut hasher3 = SimpleHasher::default();
617
618        host1.hash(&mut hasher1);
619        host2.hash(&mut hasher2);
620        host3.hash(&mut hasher3);
621
622        assert_eq!(hasher1.finish(), hasher2.finish());
623        assert_ne!(hasher1.finish(), hasher3.finish());
624    }
625
626    #[test]
627    fn test_parse_ipv4_private() {
628        let host: Host = "10.0.0.1".parse().unwrap();
629        assert!(host.is_ipaddr());
630    }
631
632    #[test]
633    fn test_parse_ipv6_loopback() {
634        let host: Host = "::1".parse().unwrap();
635        assert!(host.is_ipaddr());
636    }
637
638    #[test]
639    fn test_parse_ipv6_full() {
640        let host: Host = "2001:0db8:85a3:0000:0000:8a2e:0370:7334".parse().unwrap();
641        assert!(host.is_ipaddr());
642    }
643
644    #[test]
645    fn test_parse_domainname_numeric_label() {
646        let host: Host = "123.456.789".parse().unwrap();
647        assert!(host.is_domainname());
648    }
649
650    #[test]
651    fn test_parse_hostname_multi_label() {
652        // Note: With parsing order IpAddr -> DomainName -> Hostname,
653        // letter-start labels are parsed as DomainName (not Hostname)
654        let host: Host = "api.v1.example.com".parse().unwrap();
655        assert!(host.is_domainname());
656    }
657
658    #[test]
659    fn test_error_display() {
660        let err = HostError::InvalidInput;
661        assert_eq!(format!("{err}"), "invalid host");
662    }
663
664    #[test]
665    fn test_parse_str_method() {
666        let host = Host::parse_str("192.168.1.1").unwrap();
667        assert!(host.is_ipaddr());
668
669        // Note: With parsing order IpAddr -> DomainName -> Hostname,
670        // letter-start labels are parsed as DomainName (not Hostname)
671        let host = Host::parse_str("www.example.com").unwrap();
672        assert!(host.is_domainname());
673    }
674
675    #[test]
676    fn test_case_insensitive_hostname() {
677        // Note: With parsing order IpAddr -> DomainName -> Hostname,
678        // letter-start labels are parsed as DomainName (not Hostname)
679        let host1: Host = "WWW.EXAMPLE.COM".parse().unwrap();
680        let host2: Host = "www.example.com".parse().unwrap();
681        assert_eq!(host1, host2);
682    }
683
684    #[test]
685    fn test_case_insensitive_domainname() {
686        let host1: Host = "123.EXAMPLE.COM".parse().unwrap();
687        let host2: Host = "123.example.com".parse().unwrap();
688        assert_eq!(host1, host2);
689    }
690
691    #[test]
692    fn test_localhost_hostname() {
693        // Note: With parsing order IpAddr -> DomainName -> Hostname,
694        // letter-start labels are parsed as DomainName (not Hostname)
695        let host: Host = "localhost".parse().unwrap();
696        assert!(host.is_domainname());
697    }
698
699    #[test]
700    fn test_is_localhost() {
701        // IPv4 loopback
702        let host: Host = "127.0.0.1".parse().unwrap();
703        assert!(host.is_localhost());
704
705        // IPv6 loopback
706        let host: Host = "::1".parse().unwrap();
707        assert!(host.is_localhost());
708
709        // localhost domain name
710        let host: Host = "localhost".parse().unwrap();
711        assert!(host.is_localhost());
712
713        // Not localhost
714        let host: Host = "example.com".parse().unwrap();
715        assert!(!host.is_localhost());
716
717        let host: Host = "192.168.1.1".parse().unwrap();
718        assert!(!host.is_localhost());
719    }
720
721    #[test]
722    fn test_numeric_only_domainname() {
723        let host: Host = "123".parse().unwrap();
724        assert!(host.is_domainname());
725    }
726
727    #[test]
728    fn test_mixed_alphanumeric_hostname() {
729        // Note: With parsing order IpAddr -> DomainName -> Hostname,
730        // letter-start labels are parsed as DomainName (not Hostname)
731        let host: Host = "api-v1.example.com".parse().unwrap();
732        assert!(host.is_domainname());
733    }
734
735    #[test]
736    fn test_from_hostname_variant() {
737        // Even though parsing prioritizes DomainName, we can still create
738        // Host variants directly from Hostname
739        let hostname = Hostname::new("example.com").unwrap();
740        let host = Host::from_hostname(hostname);
741        assert!(host.is_hostname());
742        assert!(
743            host.as_hostname()
744                .map(|h: &Hostname| h.is_localhost())
745                .unwrap_or(false)
746                == false
747        );
748    }
749}