io-calendar 0.0.1

Set of I/O-free coroutines to manage calendars
use base64::{prelude::BASE64_STANDARD, Engine};
use http::{
    header::{AUTHORIZATION, CONTENT_TYPE, HOST},
    Method, Uri,
};
use secrecy::ExposeSecret;

use super::config::{CaldavAuth, CaldavConfig};

#[derive(Debug, Default)]
pub struct Request {
    builder: http::request::Builder,
}

impl Request {
    pub fn new(config: &CaldavConfig, method: Method, path: impl AsRef<str>) -> Self {
        let uri = push_uri_path(config.uri.clone().into_owned(), path);
        let mut builder = http::Request::builder().method(method).uri(uri);

        match (config.uri.host(), config.uri.port()) {
            (Some(host), Some(port)) => builder = builder.header(HOST, format!("{host}:{port}")),
            (Some(host), None) => builder = builder.header(HOST, host.to_string()),
            (None, _) => (),
        };

        match &config.auth {
            CaldavAuth::Plain => (),
            CaldavAuth::Bearer { token } => {
                let auth = format!("Bearer {}", token.expose_secret());
                builder = builder.header(AUTHORIZATION, auth);
            }
            CaldavAuth::Basic { username, password } => {
                let password = password.expose_secret();
                let digest = BASE64_STANDARD.encode(format!("{username}:{password}"));
                let auth = format!("Basic {digest}");
                builder = builder.header(AUTHORIZATION, auth);
            }
        }

        Self { builder }
    }

    pub fn delete(config: &CaldavConfig, path: impl AsRef<str>) -> Self {
        Self::new(config, Method::DELETE, path)
    }

    pub fn get(config: &CaldavConfig, path: impl AsRef<str>) -> Self {
        Self::new(config, Method::GET, path)
    }

    pub fn mkcol(config: &CaldavConfig, path: impl AsRef<str>) -> Self {
        let method = Method::from_bytes(b"MKCOL").unwrap();
        Self::new(config, method, path)
    }

    pub fn proppatch(config: &CaldavConfig, path: impl AsRef<str>) -> Self {
        let method = Method::from_bytes(b"PROPPATCH").unwrap();
        Self::new(config, method, path)
    }

    pub fn propfind(config: &CaldavConfig, path: impl AsRef<str>) -> Self {
        let method = Method::from_bytes(b"PROPFIND").unwrap();
        Self::new(config, method, path)
    }

    pub fn put(config: &CaldavConfig, path: impl AsRef<str>) -> Self {
        Self::new(config, Method::PUT, path)
    }

    pub fn report(config: &CaldavConfig, path: impl AsRef<str>) -> Self {
        let method = Method::from_bytes(b"REPORT").unwrap();
        Self::new(config, method, path)
    }

    pub fn depth(mut self, depth: usize) -> Self {
        self.builder = self.builder.header("Depth", depth);
        self
    }

    pub fn content_type(mut self, value: &str) -> Self {
        self.builder = self.builder.header(CONTENT_TYPE, value);
        self
    }

    pub fn content_type_xml(self) -> Self {
        self.content_type("text/xml; charset=utf-8")
    }

    pub fn content_type_ical(self) -> Self {
        self.content_type("text/calendar; charset=utf-8")
    }

    pub fn body(self, body: impl IntoIterator<Item = u8>) -> http::Request<Vec<u8>> {
        self.builder.body(body.into_iter().collect()).unwrap()
    }
}

pub fn set_uri_path(uri: Uri, path: impl AsRef<str>) -> Uri {
    let mut uri = uri.into_parts();
    uri.path_and_query = Some(path.as_ref().parse().unwrap());
    Uri::from_parts(uri).unwrap()
}

pub fn push_uri_path(uri: Uri, path: impl AsRef<str>) -> Uri {
    let path = path.as_ref();

    if path.is_empty() {
        return uri;
    }

    let mut uri = uri.into_parts();

    uri.path_and_query = Some(match uri.path_and_query {
        None => path.parse().unwrap(),
        Some(path_and_query) => {
            let base_path = path_and_query.path().trim_end_matches('/');
            let path = path.trim_start_matches('/');
            let mut path = format!("{base_path}/{path}");
            if let Some(query) = path_and_query.query() {
                path.push('?');
                path.push_str(query)
            }
            path.parse().unwrap()
        }
    });

    Uri::from_parts(uri).unwrap()
}