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