use crate::utils::key_value::KeyValue;
use crate::utils::string_util;
use core::fmt;
use std::borrow::{Borrow, Cow};
use std::fmt::{Display, Formatter};
use std::io;
use std::io::ErrorKind;
use std::net::{SocketAddr, ToSocketAddrs};
use std::str::FromStr;
#[derive(Clone)]
pub struct Url<'a> {
pub schema: Cow<'a, str>,
pub domain: Cow<'a, str>,
pub(crate) port: Cow<'a, str>,
pub path: Cow<'a, str>,
pub query: Vec<KeyValue<'a, Cow<'a, str>>>,
}
impl Url<'_> {
pub fn port(&self) -> u16 {
match u16::from_str(&self.port) {
Ok(port) => port,
_ if self.schema == "https" => 443,
_ => 80,
}
}
pub fn socket_address(&self) -> Result<SocketAddr, io::Error> {
self.to_socket_addrs()?
.next()
.ok_or(ErrorKind::AddrNotAvailable.into())
}
pub fn path(&self) -> String {
format!("/{}", self.path)
}
pub fn path_query(&self) -> String {
if self.query.len() > 0 {
format!("/{}?{}", self.path, self.query_string())
} else {
format!("/{}", self.path)
}
}
pub fn query_string(&self) -> String {
if self.query.len() > 0 {
self.query
.iter()
.map(|kv| kv.to_string_with_delimiter("="))
.collect::<Vec<_>>()
.join("&")
} else {
String::new()
}
}
pub fn remove_last_path_chunk(&mut self) -> Option<String> {
match self.path.rfind("/") {
Some(index) => {
let removed = self.path[index + 1..].to_owned();
self.path = self.path[..index].to_owned().into();
Some(removed)
}
None => None,
}
}
pub fn remove_query(&mut self) {
self.query.truncate(0);
}
pub fn remove_path(&mut self) {
self.path = String::new().into();
}
pub fn origin(&self) -> String {
let port = if self.port.len() > 0 {
format!(":{}", self.port)
} else {
String::new()
};
format!("{}://{}{}", self.schema, self.domain, port)
}
pub fn copy<'b>(&self) -> Url<'b> {
let mut query = Vec::with_capacity(self.query.len());
for key_value in &self.query {
query.push(KeyValue::new(
key_value.key.to_string().into(),
key_value.value.to_string().into(),
));
}
Url {
schema: self.schema.to_string().into(),
domain: self.domain.to_string().into(),
port: self.port.to_string().into(),
path: self.path.to_string().into(),
query,
}
}
}
impl<'a> From<&'a str> for Url<'a> {
fn from(url_str: &'a str) -> Self {
let (schema, rest) = match string_util::split_at_first(url_str, "://") {
None => (&url_str[..0], url_str),
Some((schema, rest)) => (schema, rest),
};
let (domain, rest) = if schema.len() > 0 {
match string_util::split_at_first(rest, "/") {
None => (rest, &rest[..0]),
Some((domain, rest)) => (domain, rest),
}
} else {
(&rest[..0], rest)
};
let (path, rest) = match string_util::split_at_first(rest, "?") {
None => (rest, &rest[..0]),
Some((path, rest)) => (path, rest),
};
let query = rest
.split("&")
.filter(|¶m| param.len() != 0)
.map(|param| {
let (key, value) = match string_util::split_at_first(param, "=") {
None => (param, ¶m[..0]),
Some((key, value)) => (key, value),
};
KeyValue::new(key.into(), value.into())
})
.collect::<Vec<KeyValue<Cow<str>>>>();
let (domain, port) = match string_util::split_at_first(domain, ":") {
None => (domain, &domain[..0]),
Some((domain, port)) => (domain, port),
};
Url {
schema: schema.into(),
domain: domain.into(),
port: port.into(),
path: path.into(),
query,
}
}
}
impl<'a, 'b> From<&Url<'a>> for Url<'b> {
fn from(value: &Url<'a>) -> Self {
value.copy()
}
}
impl ToSocketAddrs for Url<'_> {
type Iter = std::vec::IntoIter<SocketAddr>;
fn to_socket_addrs(&self) -> io::Result<Self::Iter> {
(self.domain.borrow(), self.port()).to_socket_addrs()
}
}
impl fmt::Display for Url<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let port = if self.port.len() > 0 {
format!(":{}", self.port)
} else {
String::new()
};
let path = if self.path.len() > 0 {
format!("/{}", self.path).into()
} else {
String::new()
};
let query = if self.query.len() > 0 {
format!("?{}", self.query_string())
} else {
String::new()
};
write!(
f,
"{}://{}{}{}{}",
self.schema, self.domain, port, path, query
)
}
}
impl fmt::Debug for Url<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_url_ref_from_str() -> Result<(), io::Error> {
let string = "http://build.mass.com/project/m1/viewProject.action?projectKey=TLCPLA&x=y";
let url: Url = string.into();
assert_eq!(url.schema, "http");
assert_eq!(url.domain, "build.mass.com");
assert_eq!(url.path, "project/m1/viewProject.action");
assert_eq!(url.query.len(), 2);
assert_eq!(url.query[0].key(), "projectKey");
assert_eq!(url.query[0].value(), "TLCPLA");
assert_eq!(url.query[1].key(), "x");
assert_eq!(url.query[1].value(), "y");
assert_eq!(url.port(), 80);
let string = "file:///tsla-10k_20201231_html.xml";
let url: Url = string.into();
assert_eq!(url.schema, "file");
assert_eq!(url.domain, "");
assert_eq!(url.path, "tsla-10k_20201231_html.xml");
let string = "tsla-2.xml";
let url_ref: Url = string.into();
assert_eq!(url_ref.domain, "");
assert_eq!(url_ref.path, "tsla-2.xml");
Ok(())
}
#[test]
fn test_url_to_string() -> Result<(), io::Error> {
let string = "https://build.mass.com/project/m1/viewProject.action?projectKey=TLCPLA&x=y";
let url = Url::from(string);
assert_eq!(&url.to_string(), string);
let url: Url = url.into();
assert_eq!(&url.to_string(), string);
Ok(())
}
}