use alloc::format;
use alloc::string::String;
use core::convert::TryFrom;
use core::fmt;
use core::str::FromStr;
use crate::builder::HttpUrlBuilder;
use crate::error::{HttpUrlError, Result};
use crate::scheme::Scheme;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct HttpUrl(url::Url);
impl HttpUrl {
pub fn parse(url: &str) -> Result<Self> {
Self::from_url(url::Url::parse(url)?)
}
pub fn from_url(url: url::Url) -> Result<Self> {
let _ = Scheme::from_str(url.scheme())?;
Ok(Self(url))
}
pub fn as_url(&self) -> &url::Url {
&self.0
}
pub fn into_url(self) -> url::Url {
self.0
}
pub fn builder() -> HttpUrlBuilder {
HttpUrlBuilder::new()
}
pub fn new_builder(&self) -> HttpUrlBuilder {
HttpUrlBuilder::from_url(self.0.clone())
.expect("invariant: HttpUrl scheme is always http or https")
}
pub fn scheme(&self) -> Scheme {
Scheme::from_str(self.0.scheme())
.expect("invariant: HttpUrl scheme is always http or https")
}
pub fn top_private_domain(&self) -> Option<String> {
let host = self.0.host_str()?;
if !host.bytes().any(|b| b.is_ascii_alphabetic()) {
return None;
}
let (rest, last) = host.rsplit_once('.')?;
let (_, second_last) = rest.rsplit_once('.').unwrap_or(("", rest));
Some(format!("{}.{}", second_last, last))
}
}
impl fmt::Display for HttpUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0.as_str())
}
}
impl FromStr for HttpUrl {
type Err = HttpUrlError;
fn from_str(s: &str) -> Result<Self> {
Self::parse(s)
}
}
impl From<HttpUrl> for url::Url {
fn from(http: HttpUrl) -> Self {
http.into_url()
}
}
impl TryFrom<url::Url> for HttpUrl {
type Error = HttpUrlError;
fn try_from(url: url::Url) -> Result<Self> {
Self::from_url(url)
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
#[test]
fn parse_simple() {
let url = HttpUrl::parse("http://example.com").unwrap();
assert_eq!(url.scheme(), Scheme::Http);
assert_eq!(url.as_url().host_str(), Some("example.com"));
assert_eq!(url.as_url().port(), None); assert_eq!(url.as_url().path(), "/");
}
#[test]
fn parse_https() {
let url = HttpUrl::parse("https://example.com/path").unwrap();
assert_eq!(url.scheme(), Scheme::Https);
assert_eq!(url.as_url().port(), None);
assert_eq!(url.as_url().path(), "/path");
}
#[test]
fn parse_explicit_port() {
let url = HttpUrl::parse("http://example.com:8080/").unwrap();
assert_eq!(url.as_url().port(), Some(8080));
}
#[test]
fn parse_default_port_omitted() {
let url = HttpUrl::parse("http://example.com:80/").unwrap();
assert_eq!(url.as_url().port(), None);
}
#[test]
fn parse_userinfo() {
let url = HttpUrl::parse("http://user:pass@example.com/").unwrap();
assert_eq!(url.as_url().username(), "user");
assert_eq!(url.as_url().password(), Some("pass"));
}
#[test]
fn parse_username_only() {
let url = HttpUrl::parse("http://user@example.com/").unwrap();
assert_eq!(url.as_url().username(), "user");
assert_eq!(url.as_url().password(), None);
}
#[test]
fn parse_query() {
let url = HttpUrl::parse("http://example.com/?a=1&b=2").unwrap();
let pairs: Vec<(String, String)> = url
.as_url()
.query_pairs()
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect();
assert_eq!(
pairs,
vec![
("a".to_string(), "1".to_string()),
("b".to_string(), "2".to_string())
]
);
}
#[test]
fn parse_fragment() {
let url = HttpUrl::parse("http://example.com/#section").unwrap();
assert_eq!(url.as_url().fragment(), Some("section"));
}
#[test]
fn parse_encoded_path() {
let url = HttpUrl::parse("http://example.com/hello%20world").unwrap();
assert_eq!(url.as_url().path(), "/hello%20world");
}
#[test]
fn parse_ipv6() {
let url = HttpUrl::parse("http://[::1]:8080/path").unwrap();
assert_eq!(url.as_url().host_str(), Some("[::1]"));
assert_eq!(url.as_url().port(), Some(8080));
}
#[test]
fn parse_rejects_empty() {
assert!(HttpUrl::parse("").is_err());
}
#[test]
fn parse_rejects_other_schemes() {
assert!(HttpUrl::parse("ftp://example.com").is_err());
assert!(HttpUrl::parse("file:///etc/passwd").is_err());
}
#[test]
fn parse_rejects_missing_host() {
assert!(HttpUrl::parse("http://").is_err());
}
#[test]
fn from_str_roundtrip() {
let inputs = [
"http://example.com/",
"https://example.com/path/to?q=1&r=2#frag",
"http://user:pass@host.com:8080/a/b/c",
"http://[::1]:9090/path",
"http://example.com/hello%20world",
];
for input in &inputs {
let url: HttpUrl = input.parse().unwrap();
assert_eq!(url.to_string(), *input);
}
}
#[test]
fn display_matches_url_string() {
let url = HttpUrl::parse("https://example.com/a?b=2#c").unwrap();
assert_eq!(url.to_string(), url.as_url().as_str());
}
#[test]
fn resolve_absolute() {
let base = HttpUrl::parse("http://example.com/a/b").unwrap();
let resolved =
HttpUrl::from_url(base.as_url().join("http://other.com/c").unwrap()).unwrap();
assert_eq!(resolved.as_url().host_str(), Some("other.com"));
assert_eq!(resolved.as_url().path(), "/c");
}
#[test]
fn resolve_relative() {
let base = HttpUrl::parse("http://example.com/a/b").unwrap();
let resolved = HttpUrl::from_url(base.as_url().join("c").unwrap()).unwrap();
assert_eq!(resolved.as_url().path(), "/a/c");
}
#[test]
fn resolve_absolute_path() {
let base = HttpUrl::parse("http://example.com/a/b").unwrap();
let resolved = HttpUrl::from_url(base.as_url().join("/c/d").unwrap()).unwrap();
assert_eq!(resolved.as_url().path(), "/c/d");
}
#[test]
fn resolve_protocol_relative() {
let base = HttpUrl::parse("http://example.com/a/b").unwrap();
let resolved = HttpUrl::from_url(base.as_url().join("//other.com/c").unwrap()).unwrap();
assert_eq!(resolved.scheme(), Scheme::Http);
assert_eq!(resolved.as_url().host_str(), Some("other.com"));
assert_eq!(resolved.as_url().path(), "/c");
}
#[test]
fn resolve_query_only() {
let base = HttpUrl::parse("http://example.com/a/b?old=1").unwrap();
let resolved = HttpUrl::from_url(base.as_url().join("?new=2").unwrap()).unwrap();
assert_eq!(
resolved
.as_url()
.query_pairs()
.next()
.map(|(k, v)| (k.into_owned(), v.into_owned())),
Some(("new".to_string(), "2".to_string()))
);
}
#[test]
fn resolve_fragment_only() {
let base = HttpUrl::parse("http://example.com/a/b#old").unwrap();
let resolved = HttpUrl::from_url(base.as_url().join("#new").unwrap()).unwrap();
assert_eq!(resolved.as_url().fragment(), Some("new"));
}
#[test]
fn top_private_domain() {
let url = HttpUrl::parse("http://www.example.com/path").unwrap();
assert_eq!(url.top_private_domain(), Some("example.com".to_string()));
}
#[test]
fn top_private_domain_ip_is_none() {
let url = HttpUrl::parse("http://192.168.1.1/").unwrap();
assert_eq!(url.top_private_domain(), None);
}
#[test]
fn from_url_rejects_non_http() {
let u = url::Url::parse("ftp://example.com").unwrap();
assert!(HttpUrl::try_from(u).is_err());
}
#[test]
fn url_roundtrip() {
let u = url::Url::parse("https://example.com/path?q=1").unwrap();
let http = HttpUrl::try_from(u.clone()).unwrap();
assert_eq!(http.as_url(), &u);
let back: url::Url = http.into();
assert_eq!(back, u);
}
#[test]
fn new_builder_rebuilds_equal() {
let url = HttpUrl::parse("http://example.com/path?q=1#frag").unwrap();
let url2 = url.new_builder().build().unwrap();
assert_eq!(url, url2);
}
}