gitlab_butler_lib/
client.rs

1use anyhow::Result;
2use log::{debug, error};
3use reqwest::header;
4use serde::Serialize;
5use url::{Position, Url};
6
7pub struct Client {
8    pub api_server: Url,
9    pub client: reqwest::blocking::Client,
10}
11
12#[derive(Clone, Copy)]
13enum Verb {
14    Get,
15    Put,
16    Post,
17}
18
19pub struct Paginated<'t, T> {
20    client: &'t Client,
21    api_path: &'t str,
22    verb: Verb,
23    message: Option<&'t T>,
24    next_page: NextPage,
25}
26
27#[derive(Debug)]
28enum NextPage {
29    Start,
30    Next(isize),
31    End,
32}
33
34impl<'t, T> Paginated<'t, T>
35where
36    T: Serialize + Sized,
37{
38    fn new(client: &'t Client, api_path: &'t str, verb: Verb, message: Option<&'t T>) -> Self {
39        Self {
40            client,
41            api_path,
42            verb,
43            message,
44            next_page: NextPage::Start,
45        }
46    }
47    fn get_next_page(&mut self) -> Result<Option<reqwest::blocking::Response>> {
48        let update_next_page = |resp: &reqwest::blocking::Response| -> Result<NextPage> {
49            let next_page = resp
50                .headers()
51                .get("x-next-page")
52                .map(|p| p.to_str().unwrap_or(""));
53            let next_page = match next_page {
54                None | Some("") => NextPage::End,
55                Some(num) => NextPage::Next(num.parse::<isize>()?),
56            };
57            Ok(next_page)
58        };
59        match self.next_page {
60            NextPage::Start => {
61                let resp = self
62                    .client
63                    .send(self.api_path, self.verb, self.message, None)?;
64                self.next_page = update_next_page(&resp)?;
65                Ok(Some(resp))
66            }
67            NextPage::Next(page) => {
68                let resp = self
69                    .client
70                    .send(self.api_path, self.verb, self.message, Some(page))?;
71                self.next_page = update_next_page(&resp)?;
72                Ok(Some(resp))
73            }
74            NextPage::End => Ok(None),
75        }
76    }
77}
78
79impl<'t, T> Iterator for Paginated<'t, T>
80where
81    T: Serialize + Sized,
82{
83    type Item = Result<reqwest::blocking::Response>;
84    fn next(&mut self) -> Option<Self::Item> {
85        match self.get_next_page() {
86            Ok(Some(res)) => Some(Ok(res)),
87            Ok(None) => None,
88            Err(err) => Some(Err(err)),
89        }
90    }
91}
92
93impl Client {
94    pub fn new(api_server: Url, private_token: &[u8]) -> Result<Client> {
95        let mut headers = header::HeaderMap::new();
96        headers.insert(
97            "PRIVATE-TOKEN",
98            header::HeaderValue::from_bytes(private_token)?,
99        );
100        let client = reqwest::blocking::Client::builder()
101            .default_headers(headers)
102            .build()?;
103        let api_server = Url::parse(&api_server[..Position::BeforePath])?;
104        Ok(Client { api_server, client })
105    }
106    fn send<T>(
107        &self,
108        api_path: &str,
109        verb: Verb,
110        message: Option<&T>,
111        page: Option<isize>,
112    ) -> Result<reqwest::blocking::Response>
113    where
114        T: Serialize + ?Sized,
115    {
116        let mut full_url = self.api_server.join(api_path)?;
117        if let Some(num) = page {
118            full_url
119                .query_pairs_mut()
120                .append_pair("page", &format!("{}", num));
121        }
122        let mut builder = match verb {
123            Verb::Get => self.client.get(full_url.as_str()),
124            Verb::Put => self.client.put(full_url.as_str()),
125            Verb::Post => self.client.post(full_url.as_str()),
126        };
127        if let Some(message) = message {
128            builder = builder.json(message);
129        }
130        debug!("{:?}", builder);
131        Ok(builder
132            .send()
133            .map_err(|err| {
134                error!("{}", err);
135                err
136            })?
137            .error_for_status()?)
138    }
139    pub fn put_json<T>(&self, api_path: &str, message: &T) -> Result<reqwest::blocking::Response>
140    where
141        T: Serialize + ?Sized,
142    {
143        self.send(api_path, Verb::Put, Some(message), None)
144    }
145    pub fn post_json<T>(&self, api_path: &str, message: &T) -> Result<reqwest::blocking::Response>
146    where
147        T: Serialize + ?Sized,
148    {
149        self.send(api_path, Verb::Post, Some(message), None)
150    }
151    pub fn get_json<T>(&self, api_path: &str, message: &T) -> Result<reqwest::blocking::Response>
152    where
153        T: Serialize + ?Sized,
154    {
155        self.send(api_path, Verb::Get, Some(message), None)
156    }
157    pub fn put(&self, api_path: &str) -> Result<reqwest::blocking::Response> {
158        // FIXME: this ::<&str> annotation is not really used in the None branch,
159        //        but still requested by the compiler
160        self.send::<&str>(api_path, Verb::Put, None, None)
161    }
162    pub fn get(&self, api_path: &str) -> Result<reqwest::blocking::Response> {
163        self.send::<&str>(api_path, Verb::Get, None, None)
164    }
165    pub fn get_paginated<'c>(&'c self, api_path: &'c str) -> Paginated<()> {
166        Paginated::new(&self, api_path, Verb::Get, None)
167    }
168    pub fn get_json_paginated<'c, T>(&'c self, api_path: &'c str, message: &'c T) -> Paginated<T>
169    where
170        T: Serialize + Sized,
171    {
172        Paginated::new(&self, api_path, Verb::Get, Some(message))
173    }
174}