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}