use core::str;
use std::fmt;
use std::str::FromStr;
use regex::Regex;
use super::error::HttpError;
use super::param::Param;
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct UrlError {
pub url: String,
pub reason: String,
}
impl UrlError {
fn new(url: &str, reason: &str) -> Self {
UrlError {
url: url.to_string(),
reason: reason.to_string(),
}
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Url {
raw: String,
inner: url::Url,
}
impl Default for Url {
fn default() -> Self {
Url::from_str("https://localhost").unwrap()
}
}
impl Url {
pub fn raw(&self) -> String {
self.raw.clone()
}
pub fn query_params(&self) -> Vec<Param> {
self.inner
.query_pairs()
.map(|(k, v)| Param::new(&k, &v))
.collect()
}
pub fn host(&self) -> String {
self.inner
.host()
.expect("HTTP and HTTPS URL must have a domain")
.to_string()
}
pub fn scheme(&self) -> &str {
self.inner.scheme()
}
pub fn port(&self) -> Option<u16> {
self.inner.port().or_else(|| match self.scheme() {
"http" | "ws" => Some(80),
"https" | "wss" => Some(443),
"ftp" => Some(21),
_ => None,
})
}
pub fn domain(&self) -> Option<&str> {
self.inner.domain()
}
pub fn path(&self) -> &str {
self.inner.path()
}
pub fn join(&self, input: &str) -> Result<Url, UrlError> {
let new_inner = self.inner.join(input);
let new_inner = match new_inner {
Ok(u) => u,
Err(_) => {
let error = UrlError::new(
self.inner.as_str(),
&format!("Can not use relative path '{input}'"),
);
return Err(error);
}
};
new_inner.as_str().parse()
}
}
impl FromStr for Url {
type Err = UrlError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.starts_with("https://") || value.starts_with("http://") {
let raw = value.to_string();
let inner = url::Url::parse(&raw).map_err(|e| UrlError::new(value, &e.to_string()))?;
Ok(Url { raw, inner })
} else {
match try_scheme(value) {
Some(_) => Err(UrlError::new(
value,
"Only <http://> and <https://> schemes are supported",
)),
None => Err(UrlError::new(
value,
"Missing scheme <http://> or <https://>",
)),
}
}
}
}
fn try_scheme(url: &str) -> Option<String> {
let re = Regex::new("^([a-z]+://).*").unwrap();
if let Some(caps) = re.captures(url) {
let scheme = &caps[1];
Some(scheme.to_string())
} else {
None
}
}
impl fmt::Display for Url {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.inner)
}
}
impl From<UrlError> for HttpError {
fn from(error: UrlError) -> Self {
HttpError::InvalidUrl(error.url, error.reason)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::{Url, UrlError, try_scheme};
use crate::http::Param;
#[test]
fn parse_url_ok() {
let urls = [
"http://localhost:8000/hello",
"http://localhost:8000/querystring-params?param1=value1¶m2=¶m3=a%3Db¶m4=1%2C2%2C3",
"http://localhost:8000/cookies",
"http://localhost",
"https://localhost:8000",
"http://localhost:8000/path-as-is/../resource",
];
for url in urls {
assert!(Url::from_str(url).is_ok());
}
}
#[test]
fn query_params() {
let url: Url = "http://localhost:8000/hello".parse().unwrap();
assert_eq!(url.query_params(), vec![]);
let url: Url = "http://localhost:8000/querystring-params?param1=value1¶m2=¶m3=a%3Db¶m4=1%2C2%2C3".parse().unwrap();
assert_eq!(
url.query_params(),
vec![
Param::new("param1", "value1"),
Param::new("param2", ""),
Param::new("param3", "a=b"),
Param::new("param4", "1,2,3"),
]
);
}
#[test]
fn test_join() {
let base: Url = "http://example.net/foo/index.html".parse().unwrap();
assert_eq!(
base.join("http://bar.com/redirected").unwrap(),
"http://bar.com/redirected".parse().unwrap()
);
assert_eq!(
base.join("/redirected").unwrap(),
"http://example.net/redirected".parse().unwrap()
);
assert_eq!(
base.join("../bar/index.html").unwrap(),
"http://example.net/bar/index.html".parse().unwrap()
);
assert_eq!(
base.join("//example.org/baz/index.html").unwrap(),
"http://example.org/baz/index.html".parse().unwrap()
);
}
#[test]
fn test_parsing_error() {
assert_eq!(
Url::from_str("localhost:8000").err().unwrap(),
UrlError::new("localhost:8000", "Missing scheme <http://> or <https://>")
);
assert_eq!(
Url::from_str("file://localhost:8000").err().unwrap(),
UrlError::new(
"file://localhost:8000",
"Only <http://> and <https://> schemes are supported"
)
);
}
#[test]
fn test_extract_scheme() {
assert!(try_scheme("localhost:8000").is_none());
assert!(try_scheme("http1://localhost:8000").is_none());
assert!(try_scheme("://localhost:8000").is_none());
assert_eq!(try_scheme("file://data").unwrap(), "file://".to_string());
assert_eq!(try_scheme("http://data").unwrap(), "http://".to_string());
}
}