gitlab-butler-lib 0.9.1

Support library for [`gitlab-butler`](https://crates.io/crates/gitlab-butler) Gitlab cli to automate the boring stuff.
Documentation
use anyhow::Result;
use log::{debug, error};
use reqwest::header;
use serde::Serialize;
use url::{Position, Url};

pub struct Client {
    pub api_server: Url,
    pub client: reqwest::blocking::Client,
}

#[derive(Clone, Copy)]
enum Verb {
    Get,
    Put,
    Post,
}

pub struct Paginated<'t, T> {
    client: &'t Client,
    api_path: &'t str,
    verb: Verb,
    message: Option<&'t T>,
    next_page: NextPage,
}

#[derive(Debug)]
enum NextPage {
    Start,
    Next(isize),
    End,
}

impl<'t, T> Paginated<'t, T>
where
    T: Serialize + Sized,
{
    fn new(client: &'t Client, api_path: &'t str, verb: Verb, message: Option<&'t T>) -> Self {
        Self {
            client,
            api_path,
            verb,
            message,
            next_page: NextPage::Start,
        }
    }
    fn get_next_page(&mut self) -> Result<Option<reqwest::blocking::Response>> {
        let update_next_page = |resp: &reqwest::blocking::Response| -> Result<NextPage> {
            let next_page = resp
                .headers()
                .get("x-next-page")
                .map(|p| p.to_str().unwrap_or(""));
            let next_page = match next_page {
                None | Some("") => NextPage::End,
                Some(num) => NextPage::Next(num.parse::<isize>()?),
            };
            Ok(next_page)
        };
        match self.next_page {
            NextPage::Start => {
                let resp = self
                    .client
                    .send(self.api_path, self.verb, self.message, None)?;
                self.next_page = update_next_page(&resp)?;
                Ok(Some(resp))
            }
            NextPage::Next(page) => {
                let resp = self
                    .client
                    .send(self.api_path, self.verb, self.message, Some(page))?;
                self.next_page = update_next_page(&resp)?;
                Ok(Some(resp))
            }
            NextPage::End => Ok(None),
        }
    }
}

impl<'t, T> Iterator for Paginated<'t, T>
where
    T: Serialize + Sized,
{
    type Item = Result<reqwest::blocking::Response>;
    fn next(&mut self) -> Option<Self::Item> {
        match self.get_next_page() {
            Ok(Some(res)) => Some(Ok(res)),
            Ok(None) => None,
            Err(err) => Some(Err(err)),
        }
    }
}

impl Client {
    pub fn new(api_server: Url, private_token: &[u8]) -> Result<Client> {
        let mut headers = header::HeaderMap::new();
        headers.insert(
            "PRIVATE-TOKEN",
            header::HeaderValue::from_bytes(private_token)?,
        );
        let client = reqwest::blocking::Client::builder()
            .default_headers(headers)
            .build()?;
        let api_server = Url::parse(&api_server[..Position::BeforePath])?;
        Ok(Client { api_server, client })
    }
    fn send<T>(
        &self,
        api_path: &str,
        verb: Verb,
        message: Option<&T>,
        page: Option<isize>,
    ) -> Result<reqwest::blocking::Response>
    where
        T: Serialize + ?Sized,
    {
        let mut full_url = self.api_server.join(api_path)?;
        if let Some(num) = page {
            full_url
                .query_pairs_mut()
                .append_pair("page", &format!("{}", num));
        }
        let mut builder = match verb {
            Verb::Get => self.client.get(full_url.as_str()),
            Verb::Put => self.client.put(full_url.as_str()),
            Verb::Post => self.client.post(full_url.as_str()),
        };
        if let Some(message) = message {
            builder = builder.json(message);
        }
        debug!("{:?}", builder);
        Ok(builder
            .send()
            .map_err(|err| {
                error!("{}", err);
                err
            })?
            .error_for_status()?)
    }
    pub fn put_json<T>(&self, api_path: &str, message: &T) -> Result<reqwest::blocking::Response>
    where
        T: Serialize + ?Sized,
    {
        self.send(api_path, Verb::Put, Some(message), None)
    }
    pub fn post_json<T>(&self, api_path: &str, message: &T) -> Result<reqwest::blocking::Response>
    where
        T: Serialize + ?Sized,
    {
        self.send(api_path, Verb::Post, Some(message), None)
    }
    pub fn get_json<T>(&self, api_path: &str, message: &T) -> Result<reqwest::blocking::Response>
    where
        T: Serialize + ?Sized,
    {
        self.send(api_path, Verb::Get, Some(message), None)
    }
    pub fn put(&self, api_path: &str) -> Result<reqwest::blocking::Response> {
        // FIXME: this ::<&str> annotation is not really used in the None branch,
        //        but still requested by the compiler
        self.send::<&str>(api_path, Verb::Put, None, None)
    }
    pub fn get(&self, api_path: &str) -> Result<reqwest::blocking::Response> {
        self.send::<&str>(api_path, Verb::Get, None, None)
    }
    pub fn get_paginated<'c>(&'c self, api_path: &'c str) -> Paginated<()> {
        Paginated::new(&self, api_path, Verb::Get, None)
    }
    pub fn get_json_paginated<'c, T>(&'c self, api_path: &'c str, message: &'c T) -> Paginated<T>
    where
        T: Serialize + Sized,
    {
        Paginated::new(&self, api_path, Verb::Get, Some(message))
    }
}