Skip to content

Commit bb57e50

Browse files
authored
Merge pull request #22 from ail-project/refactor/host-api-improvements
refactor: Improve host handling and add comprehensive documentation fix #19
2 parents 9b63975 + e5988fa commit bb57e50

File tree

4 files changed

+177
-54
lines changed

4 files changed

+177
-54
lines changed

faup/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1036,7 +1036,10 @@ pub struct Url<'url> {
10361036
orig: Cow<'url, str>,
10371037
scheme: Cow<'url, str>,
10381038
userinfo: Option<UserInfo<'url>>,
1039-
host: Option<Host<'url>>,
1039+
/// The host component of the URL, which can be a hostname, IPv4 address, or IPv6 address.
1040+
///
1041+
/// This field is `None` if the URL does not contain a host component.
1042+
pub host: Option<Host<'url>>,
10401043
port: Option<u16>,
10411044
path: Option<Cow<'url, str>>,
10421045
query: Option<Cow<'url, str>>,

python/pyfaup.pyi

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class Host:
2121
def is_ipv4(self) -> bool: ...
2222
def is_ipv6(self) -> bool: ...
2323
def is_ip_addr(self) -> bool: ...
24+
def domain(self) -> Optional[str]: ...
25+
def subdomain(self) -> Optional[str]: ...
26+
def suffix(self) -> Optional[Suffix]: ...
2427
def __str__(self) -> str: ...
2528

2629
class FaupCompat:
@@ -46,10 +49,7 @@ class Url:
4649
scheme: str
4750
username: Optional[str]
4851
password: Optional[str]
49-
host: Optional[str]
50-
subdomain: Optional[str]
51-
domain: Optional[str]
52-
suffix: Optional[Suffix]
52+
host: Optional[Host]
5353
port: Optional[int]
5454
path: Optional[str]
5555
query: Optional[str]

python/src/lib.rs

Lines changed: 137 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::net::IpAddr;
1+
use std::{fmt::Display, net::IpAddr};
22

33
use pyo3::{
44
basic::CompareOp,
@@ -201,6 +201,7 @@ impl Hostname {
201201

202202
/// Represents a host, which can be either a [`Hostname`] or an [`IpAddr`].
203203
#[pyclass]
204+
#[derive(Clone)]
204205
pub enum Host {
205206
/// A hostname (domain name).
206207
Hostname(Hostname),
@@ -220,6 +221,26 @@ impl From<faup_rs::Host<'_>> for Host {
220221
}
221222
}
222223

224+
impl Display for Host {
225+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226+
match self {
227+
Self::Hostname(h) => write!(f, "{}", h.hostname.clone()),
228+
Self::Ipv4(ip) => write!(f, "{ip}"),
229+
Self::Ipv6(ip) => write!(f, "{ip}"),
230+
}
231+
}
232+
}
233+
234+
impl Host {
235+
#[inline(always)]
236+
fn suffix_ref(&self) -> Option<&Suffix> {
237+
match self {
238+
Host::Hostname(hostname) => hostname.suffix.as_ref(),
239+
Host::Ipv4(_ip_addr) | Host::Ipv6(_ip_addr) => None,
240+
}
241+
}
242+
}
243+
223244
#[pymethods]
224245
impl Host {
225246
/// Creates a new [`Host`] by parsing a host string.
@@ -359,13 +380,81 @@ impl Host {
359380
self.is_ipv4() | self.is_ipv6()
360381
}
361382

362-
pub fn __str__(&self) -> String {
383+
/// Returns the domain part of the hostname if this host is a hostname.
384+
///
385+
/// Returns `None` if this host is an IP address or if the hostname has no recognized domain.
386+
///
387+
/// # Returns
388+
///
389+
/// * `Optional[str]` - The domain part of the hostname, or `None` if not applicable.
390+
///
391+
/// # Example
392+
///
393+
/// >>> from pyfaup import Host
394+
/// >>> host = Host("sub.example.com")
395+
/// >>> print(host.domain()) # "example.com"
396+
/// >>> print(Host("192.168.1.1").domain()) # None
397+
pub fn domain(&self) -> Option<&str> {
363398
match self {
364-
Self::Hostname(h) => h.hostname.clone(),
365-
Self::Ipv4(ip) => ip.to_string(),
366-
Self::Ipv6(ip) => ip.to_string(),
399+
Host::Hostname(hostname) => hostname.domain.as_ref().map(String::as_ref),
400+
Host::Ipv4(_ip_addr) | Host::Ipv6(_ip_addr) => None,
367401
}
368402
}
403+
404+
/// Returns the subdomain part of the hostname if this host is a hostname.
405+
///
406+
/// Returns `None` if this host is an IP address or if the hostname has no subdomain.
407+
///
408+
/// # Returns
409+
///
410+
/// * `Optional[str]` - The subdomain part of the hostname, or `None` if not applicable.
411+
///
412+
/// # Example
413+
///
414+
/// >>> from pyfaup import Host
415+
/// >>> host = Host("sub.example.com")
416+
/// >>> print(host.subdomain()) # "sub"
417+
/// >>> print(Host("example.com").subdomain()) # None
418+
/// >>> print(Host("192.168.1.1").subdomain()) # None
419+
pub fn subdomain(&self) -> Option<&str> {
420+
match self {
421+
Host::Hostname(hostname) => hostname.subdomain.as_ref().map(String::as_ref),
422+
Host::Ipv4(_ip_addr) | Host::Ipv6(_ip_addr) => None,
423+
}
424+
}
425+
426+
/// Returns the suffix (public suffix) of the hostname if this host is a hostname.
427+
///
428+
/// Returns `None` if this host is an IP address or if the hostname has no recognized suffix.
429+
///
430+
/// # Returns
431+
///
432+
/// * `Optional[Suffix]` - The suffix of the hostname, or `None` if not applicable.
433+
///
434+
/// # Example
435+
///
436+
/// >>> from pyfaup import Host
437+
/// >>> host = Host("sub.example.com")
438+
/// >>> print(host.suffix()) # Suffix(com, True)
439+
/// >>> print(Host("192.168.1.1").suffix()) # None
440+
pub fn suffix(&self) -> Option<Suffix> {
441+
self.suffix_ref().cloned()
442+
}
443+
444+
/// Returns the string representation of the host.
445+
///
446+
/// # Returns
447+
///
448+
/// * `str` - The string representation of the host.
449+
///
450+
/// # Example
451+
///
452+
/// >>> from pyfaup import Host
453+
/// >>> print(str(Host("example.com"))) # "example.com"
454+
/// >>> print(str(Host("192.168.1.1"))) # "192.168.1.1"
455+
pub fn __str__(&self) -> String {
456+
self.to_string()
457+
}
369458
}
370459

371460
/// A parsed URL representation for Python.
@@ -405,13 +494,7 @@ pub struct Url {
405494
#[pyo3(get)]
406495
pub password: Option<String>,
407496
#[pyo3(get)]
408-
pub host: Option<String>,
409-
#[pyo3(get)]
410-
pub subdomain: Option<String>,
411-
#[pyo3(get)]
412-
pub domain: Option<String>,
413-
#[pyo3(get)]
414-
pub suffix: Option<Suffix>,
497+
pub host: Option<Host>,
415498
#[pyo3(get)]
416499
pub port: Option<u16>,
417500
#[pyo3(get)]
@@ -424,10 +507,6 @@ pub struct Url {
424507

425508
impl From<faup_rs::Url<'_>> for Url {
426509
fn from(value: faup_rs::Url<'_>) -> Self {
427-
let mut subdomain = None;
428-
let mut domain = None;
429-
let mut suffix = None;
430-
431510
let (username, password) = match value.userinfo() {
432511
Some(u) => (
433512
Some(u.username().to_string()),
@@ -436,31 +515,24 @@ impl From<faup_rs::Url<'_>> for Url {
436515
None => (None, None),
437516
};
438517

439-
let host = match value.host() {
440-
Some(faup_rs::Host::Hostname(hostname)) => {
441-
subdomain = hostname.subdomain().map(|s| s.into());
442-
domain = hostname.domain().map(|d| d.into());
443-
suffix = hostname.suffix().map(|s| s.into());
444-
Some(hostname.full_name().into())
445-
}
446-
Some(faup_rs::Host::IpV4(ip)) => Some(ip.to_string()),
447-
Some(faup_rs::Host::IpV6(ip, _)) => Some(ip.to_string()),
448-
None => None,
449-
};
518+
let orig = value.as_str().into();
519+
let scheme = value.scheme().into();
520+
let port = value.port();
521+
let path = value.path().map(|p| p.into());
522+
let query = value.query().map(|q| q.into());
523+
let fragment = value.fragment().map(|f| f.into());
524+
let host = value.host.map(Host::from);
450525

451526
Self {
452-
orig: value.as_str().into(),
453-
scheme: value.scheme().into(),
527+
orig,
528+
scheme,
454529
username,
455530
password,
456531
host,
457-
subdomain,
458-
domain,
459-
suffix,
460-
port: value.port(),
461-
path: value.path().map(|p| p.into()),
462-
query: value.query().map(|q| q.into()),
463-
fragment: value.fragment().map(|f| f.into()),
532+
port,
533+
path,
534+
query,
535+
fragment,
464536
}
465537
}
466538
}
@@ -584,12 +656,22 @@ impl FaupCompat {
584656
let credentials = url.and_then(|u| u.credentials());
585657

586658
m.set_item("credentials", credentials)?;
587-
m.set_item("domain", url.and_then(|u| u.domain.clone()))?;
588-
m.set_item("subdomain", url.and_then(|u| u.subdomain.clone()))?;
659+
m.set_item(
660+
"domain",
661+
url.and_then(|u| u.host.as_ref()).and_then(|h| h.domain()),
662+
)?;
663+
m.set_item(
664+
"subdomain",
665+
url.and_then(|u| u.host.as_ref())
666+
.and_then(|h| h.subdomain()),
667+
)?;
589668
m.set_item("fragment", url.and_then(|u| u.fragment.clone()))?;
590669
m.set_item("host", url.map(|u| u.host.clone()))?;
591670
m.set_item("resource_path", url.and_then(|u| u.path.clone()))?;
592-
m.set_item("tld", url.and_then(|u| u.suffix.clone()))?;
671+
m.set_item(
672+
"tld",
673+
url.and_then(|u| u.host.as_ref()).and_then(|h| h.suffix()),
674+
)?;
593675
m.set_item("query_string", url.and_then(|u| u.query.clone()))?;
594676
m.set_item("scheme", url.map(|u| u.scheme.clone()))?;
595677
m.set_item("port", url.map(|u| u.port))?;
@@ -603,30 +685,40 @@ impl FaupCompat {
603685
}
604686

605687
fn get_domain(&self) -> Option<&str> {
606-
self.url.as_ref()?.domain.as_deref()
688+
self.url
689+
.as_ref()
690+
.and_then(|u| u.host.as_ref())
691+
.and_then(|h| h.domain())
607692
}
608693

609694
fn get_subdomain(&self) -> Option<&str> {
610-
self.url.as_ref()?.subdomain.as_deref()
695+
self.url
696+
.as_ref()
697+
.and_then(|u| u.host.as_ref())
698+
.and_then(|h| h.subdomain())
611699
}
612700

613701
fn get_fragment(&self) -> Option<&str> {
614702
self.url.as_ref()?.fragment.as_deref()
615703
}
616704

617-
fn get_host(&self) -> Option<&str> {
705+
fn get_host(&self) -> Option<String> {
618706
self.url
619707
.as_ref()
620-
.map(|u| u.host.as_ref())?
621-
.map(|h| h.as_ref())
708+
.and_then(|u| u.host.as_ref())
709+
.map(|h| h.to_string())
622710
}
623711

624712
fn get_resource_path(&self) -> Option<&str> {
625713
self.url.as_ref()?.path.as_deref()
626714
}
627715

628716
fn get_tld(&self) -> Option<&str> {
629-
self.url.as_ref()?.suffix.as_ref().map(|s| s.value.as_str())
717+
self.url
718+
.as_ref()
719+
.and_then(|u| u.host.as_ref())
720+
.and_then(|h| h.suffix_ref())
721+
.map(|s| s.value.as_str())
630722
}
631723

632724
fn get_query_string(&self) -> Option<&str> {

python/tests/test_pyfaup.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ def test_http_url(self) -> None:
2121
self.assertEqual(parsed_url.scheme, "https")
2222
self.assertEqual(parsed_url.username, "user")
2323
self.assertEqual(parsed_url.password, "pass")
24-
self.assertEqual(parsed_url.host, "sub.example.com")
25-
self.assertEqual(parsed_url.subdomain, "sub")
26-
self.assertEqual(parsed_url.domain, "example.com")
27-
self.assertEqual(parsed_url.suffix, "com")
24+
self.assertEqual(str(parsed_url.host), "sub.example.com")
25+
if parsed_url.host is not None:
26+
self.assertEqual(parsed_url.host.subdomain(), "sub")
27+
if parsed_url.host is not None:
28+
self.assertEqual(parsed_url.host.domain(), "example.com")
29+
if parsed_url.host is not None:
30+
self.assertEqual(parsed_url.host.suffix(), "com")
2831
self.assertEqual(parsed_url.port, 8080)
2932
self.assertEqual(parsed_url.path, "/path")
3033
self.assertEqual(parsed_url.query, "query=value")
@@ -103,6 +106,31 @@ def test_host(self) -> None:
103106
with self.assertRaises(ValueError):
104107
host.try_into_ip()
105108

109+
# Test domain(), subdomain(), and suffix() methods
110+
host = Host("sub.example.com")
111+
self.assertEqual(host.domain(), "example.com")
112+
self.assertEqual(host.subdomain(), "sub")
113+
self.assertIsNotNone(host.suffix())
114+
self.assertEqual(str(host.suffix()), "com")
115+
116+
# Test domain(), subdomain(), and suffix() with simple domain
117+
host = Host("example.com")
118+
self.assertEqual(host.domain(), "example.com")
119+
self.assertIsNone(host.subdomain())
120+
self.assertIsNotNone(host.suffix())
121+
self.assertEqual(str(host.suffix()), "com")
122+
123+
# Test domain(), subdomain(), and suffix() with IP addresses
124+
host = Host("192.168.1.1")
125+
self.assertIsNone(host.domain())
126+
self.assertIsNone(host.subdomain())
127+
self.assertIsNone(host.suffix())
128+
129+
host = Host("::1")
130+
self.assertIsNone(host.domain())
131+
self.assertIsNone(host.subdomain())
132+
self.assertIsNone(host.suffix())
133+
106134
# Test invalid host
107135
with self.assertRaises(ValueError):
108136
Host("invalid host")

0 commit comments

Comments
 (0)