httpio 0.2.4

A transport-agnostic, async HTTP/1.1 client library for any runtime.
Documentation
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(|&param| param.len() != 0)
            .map(|param| {
                let (key, value) = match string_util::split_at_first(param, "=") {
                    None => (param, &param[..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(())
    }
}