circulo/
lib.rs

1mod api_token;
2mod insights;
3mod pipeline;
4mod project;
5
6use reqwest::{
7    header::{HeaderMap, HeaderValue, InvalidHeaderValue},
8    Response,
9};
10use std::{
11    env::{self, VarError},
12    fmt::Debug,
13};
14use thiserror::Error;
15
16pub use api_token::ApiToken;
17pub use insights::*;
18pub use pipeline::*;
19pub use project::*;
20
21const API_URL: &str = "https://circleci.com/api/v2";
22
23pub struct Client {
24    url: String,
25    client: reqwest::Client,
26}
27
28impl Debug for Client {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        f.debug_struct("Client")
31            .field("url", &self.url)
32            .field("api_token", &"<redacted>")
33            .finish()
34    }
35}
36
37#[derive(Debug, Error)]
38pub enum Error {
39    #[error("invalid header")]
40    InvalidHeader(#[from] InvalidHeaderValue),
41
42    #[error("reqwest error")]
43    Reqwest(#[from] reqwest::Error),
44
45    #[error("something wrong with the env var")]
46    EnvVar(#[from] VarError),
47
48    #[error("received an error code from an HTTP request")]
49    Http(String),
50}
51
52type Result<T> = std::result::Result<T, Error>;
53
54impl Client {
55    /// # Errors
56    ///
57    /// Returns an error if the api token is not a valid header or there's an
58    /// issue constructing the client.
59    pub fn new(api_token: &ApiToken) -> Result<Self> {
60        let mut headers = HeaderMap::new();
61        let mut auth_value = HeaderValue::from_str(api_token)?;
62        auth_value.set_sensitive(true);
63        headers.insert("Circle-Token", auth_value);
64
65        let client = reqwest::Client::builder()
66            .default_headers(headers)
67            .build()?;
68
69        Ok(Self {
70            url: env::var("CIRCLECI_API_URL")
71                .unwrap_or_else(|_| API_URL.to_string()),
72            client,
73        })
74    }
75
76    /// # Errors
77    ///
78    /// Returns an error if anything goes wrong with the request.
79    pub async fn get<T: Requestable>(&self, params: &T) -> Result<Response> {
80        let req = self
81            .client
82            .get(params.path(&self.url))
83            .query(&params.query_params());
84
85        let result = req.send().await?;
86        if !result.status().is_success() {
87            return Err(Error::Http(result.text().await?));
88        }
89
90        Ok(result)
91    }
92
93    /// Send an HTTP DELETE request.
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if anything goes wrong with the request.
98    pub async fn delete<T: Requestable>(&self, params: &T) -> Result<Response> {
99        let req = self
100            .client
101            .delete(params.path(&self.url))
102            .query(&params.query_params());
103
104        let result = req.send().await?;
105        if !result.status().is_success() {
106            return Err(Error::Http(result.text().await?));
107        }
108
109        Ok(result)
110    }
111}
112
113pub trait Requestable {
114    fn path(&self, base: &str) -> String;
115
116    fn query_params(&self) -> Vec<(&str, &str)> {
117        vec![]
118    }
119}