use core::{fmt, str::FromStr};
use crate::std::{self as std, string::String, vec::Vec};
use crate::Protocol;
use crate::address::{Domain, parse_utils};
use rama_core::error::{BoxError, ErrorContext};
use rama_utils::macros::generate_set_and_with;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DomainAddress {
pub domain: Domain,
pub port: u16,
}
impl DomainAddress {
#[must_use]
#[inline(always)]
pub const fn new(domain: Domain, port: u16) -> Self {
Self { domain, port }
}
#[must_use]
#[inline(always)]
pub const fn example_http() -> Self {
Self {
domain: Domain::example(),
port: Protocol::HTTP_DEFAULT_PORT,
}
}
#[must_use]
#[inline(always)]
pub const fn example_https() -> Self {
Self {
domain: Domain::example(),
port: Protocol::HTTPS_DEFAULT_PORT,
}
}
#[must_use]
#[inline(always)]
pub const fn example_with_port(port: u16) -> Self {
Self {
domain: Domain::example(),
port,
}
}
#[must_use]
#[inline(always)]
pub const fn localhost_http() -> Self {
Self {
domain: Domain::tld_localhost(),
port: Protocol::HTTP_DEFAULT_PORT,
}
}
#[must_use]
#[inline(always)]
pub const fn localhost_https() -> Self {
Self {
domain: Domain::tld_localhost(),
port: Protocol::HTTPS_DEFAULT_PORT,
}
}
#[must_use]
#[inline(always)]
pub const fn localhost_with_port(port: u16) -> Self {
Self {
domain: Domain::tld_localhost(),
port,
}
}
generate_set_and_with! {
pub fn domain(mut self, domain: Domain) -> Self {
self.domain = domain;
self
}
}
generate_set_and_with! {
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
}
}
impl From<(Domain, u16)> for DomainAddress {
#[inline]
fn from((domain, port): (Domain, u16)) -> Self {
Self::new(domain, port)
}
}
impl fmt::Display for DomainAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.domain, self.port)
}
}
impl FromStr for DomainAddress {
type Err = BoxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
}
}
impl TryFrom<&str> for DomainAddress {
type Error = BoxError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
let (domain, port) = parse_utils::split_port_from_str(s)?;
let domain = Domain::from_str(domain)?;
Ok(Self::new(domain, port))
}
}
impl TryFrom<String> for DomainAddress {
type Error = BoxError;
fn try_from(s: String) -> Result<Self, Self::Error> {
let (domain, port) = parse_utils::split_port_from_str(&s)?;
let domain = Domain::from_str(domain)?;
Ok(Self::new(domain, port))
}
}
impl TryFrom<Vec<u8>> for DomainAddress {
type Error = BoxError;
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
let s = String::from_utf8(bytes).context("parse domain_address from bytes")?;
s.try_into()
}
}
impl TryFrom<&[u8]> for DomainAddress {
type Error = BoxError;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let s = core::str::from_utf8(bytes).context("parse domain_address from bytes")?;
s.try_into()
}
}
use rama_utils::macros::serde_str::impl_serde_str;
impl_serde_str!(display DomainAddress);
#[cfg(test)]
mod tests {
use super::*;
#[expect(clippy::needless_pass_by_value)]
fn assert_eq(s: &str, domain_address: DomainAddress, domain: &str, port: u16) {
assert_eq!(domain_address.domain.as_str(), domain, "parsing: {s}");
assert_eq!(domain_address.port, port, "parsing: {s}");
}
#[test]
fn test_valid_domain_address() {
for (s, (expected_domain, expected_port)) in [
("example.com:80", ("example.com", 80)),
("subdomain.example.com:443", ("subdomain.example.com", 443)),
("127.0.0.1:8080", ("127.0.0.1", 8080)),
] {
let msg = format!("parsing '{s}'");
assert_eq(s, s.parse().expect(&msg), expected_domain, expected_port);
assert_eq(s, s.try_into().expect(&msg), expected_domain, expected_port);
assert_eq(
s,
s.to_owned().try_into().expect(&msg),
expected_domain,
expected_port,
);
assert_eq(
s,
s.as_bytes().try_into().expect(&msg),
expected_domain,
expected_port,
);
assert_eq(
s,
s.as_bytes().to_vec().try_into().expect(&msg),
expected_domain,
expected_port,
);
}
}
#[test]
fn test_parse_invalid() {
for s in [
"",
"-",
".",
":",
":80",
"-.",
".-",
"::1",
"127.0.0.1",
"::1:8080",
"127.0.0.1",
"[::1]",
"example",
"exa$mple.com:8080",
"2001:db8:3333:4444:5555:6666:7777:8888:8080",
"2001:db8:3333:4444:5555:6666:7777:8888",
"[2001:db8:3333:4444:5555:6666:7777:8888]",
"[2001:db8:3333:4444:5555:6666:7777:8888]:8080",
"example.com",
"example.com:",
"example.com:-1",
"example.com:999999",
"example:com",
"[127.0.0.1]:80",
"2001:db8:3333:4444:5555:6666:7777:8888:80",
] {
let msg = format!("parsing '{s}'");
assert!(s.parse::<DomainAddress>().is_err(), "{msg}");
assert!(DomainAddress::try_from(s).is_err(), "{msg}");
assert!(DomainAddress::try_from(s.to_owned()).is_err(), "{msg}");
assert!(DomainAddress::try_from(s.as_bytes()).is_err(), "{msg}");
assert!(
DomainAddress::try_from(s.as_bytes().to_vec()).is_err(),
"{msg}"
);
}
}
#[test]
fn test_parse_display() {
for (s, expected) in [
("example.com:80", "example.com:80"),
("subdomain.example.com:443", "subdomain.example.com:443"),
] {
let msg = format!("parsing '{s}'");
let domain_address: DomainAddress = s.parse().expect(&msg);
assert_eq!(domain_address.to_string(), expected, "{msg}");
}
}
}