use_github_api/client/
mod.rs

1use crate::users::Users;
2#[cfg(feature = "enterprise")]
3use crate::CreationError;
4use reqwest::{
5    header::{HeaderMap, ACCEPT},
6    Client,
7};
8use std::error::Error as StdError;
9
10#[cfg(any(feature = "auth", doc))]
11#[cfg_attr(docsrs, doc(cfg(feature = "auth")))]
12mod builder;
13
14#[cfg(any(feature = "auth", doc))]
15pub use builder::GithubClientBuilder;
16
17pub(crate) mod macros {
18    #[macro_export]
19    #[doc(hidden)]
20    macro_rules! url {
21        ($self:expr, $i:expr) => {
22            format!("{}{}", $self.client.base_url, $i)
23        };
24        ($self:expr, $i:expr, $($arg:expr),*) => {
25            format!("{}{}", $self.client.base_url, format!($i, $($arg),*))
26        };
27    }
28}
29
30#[derive(Debug)]
31/// Holds the reqwest client, auth token, base url, headers, user agent, etc.
32pub struct GithubClient<'a> {
33    pub(crate) base_url: &'a str,
34    pub(crate) reqwest_client: Client,
35    #[cfg(feature = "auth")]
36    auth_token: &'a str,
37    pub(crate) default_headers: HeaderMap,
38    user_agent: &'a str,
39}
40
41impl<'a> GithubClient<'a> {
42    /// Creates a new `GithubClient` which can be used to send requests.
43    /// # Signature
44    /// The signatures of the function changes when the features are changed.
45    /// - When no features are enabled, the signature is `fn () -> Result<GithubClient<'a>, Box<dyn StdError>>`
46    /// - When the auth feature is enabled, the signature is `fn (auth_token: &'a str) -> Result<GithubClient<'a>, Box<dyn StdError>>`
47    /// - When the enterprise feature is enabled, the signature is fn `fn (base_url: &'a str, auth_token: &'a str) -> Result<GithubClient<'a>, Box<dyn StdError>>`
48    /// # Arguments
49    /// ## Base URL
50    /// The base url can be like this: `https://somehostfor.github.enterprise.org/api/v3`.
51    /// Do make sure to add the `https://` and the `/api/v3`.
52    /// ## Auth Token
53    /// If using a PAT (personal access token), you can obtain one from <https://github.com/settings/tokens>.
54    /// # Errors
55    /// Will error if the protocol is not `http://` or `https://`, and will also error if the base URL does not include `/api/v3`.
56    /// Will also error if the reqwest client fails to build.
57    /// # Examples
58    /// ```rust
59    /// use use_github_api::GithubClient;
60    /// # #[cfg(feature = "auth")]
61    /// # #[cfg(not(feature = "enterprise"))]
62    /// let client = GithubClient::new("ghp_akjsdh").unwrap(); // DO NOT ACTUALLY HARDCODE TOKENS IN YOUR APP!!!
63    /// // do something with `client`
64    /// ```
65    pub fn new(
66        #[cfg(feature = "enterprise")] base_url: &'a str,
67        #[cfg(feature = "auth")] auth_token: &'a str,
68    ) -> Result<GithubClient<'a>, Box<dyn StdError>> {
69        #[cfg(feature = "enterprise")]
70        if !(base_url.starts_with("https://") || base_url.starts_with("http://")) {
71            return Err(CreationError::base_url_without_protocol().into());
72        }
73        #[cfg(feature = "enterprise")]
74        if !base_url.ends_with("/api/v3") {
75            return Err(CreationError::base_url_without_api_path().into());
76        }
77        let mut headers = HeaderMap::new();
78        headers.insert(ACCEPT, "application/vnd.github.v3+json".parse().unwrap());
79        #[cfg(feature = "auth")]
80        if let Ok(token_header) = format!("token {}", auth_token).parse() {
81            headers.insert("Authorization", token_header);
82        }
83        const UA: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
84        let client = Client::builder()
85            .default_headers(headers.clone())
86            .user_agent(UA)
87            .build()?;
88        Ok(Self {
89            #[cfg(feature = "enterprise")]
90            base_url,
91            #[cfg(not(feature = "enterprise"))]
92            base_url: "https://api.github.com",
93            reqwest_client: client,
94            #[cfg(feature = "auth")]
95            auth_token,
96            default_headers: headers,
97            user_agent: UA,
98        })
99    }
100
101    #[cfg(feature = "auth")]
102    /// Gives a `GithubClientBuilder`, same as using `GithubClientBuilder::new()`.
103    pub fn builder() -> GithubClientBuilder<'a> {
104        GithubClientBuilder::new()
105    }
106
107    pub fn users(&self) -> Users<'_> {
108        Users::new(&self)
109    }
110}
111
112#[cfg(not(feature = "auth"))]
113impl<'a> Default for GithubClient<'a> {
114    fn default() -> Self {
115        Self::new().expect("Error while creating default client")
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    #[cfg(feature = "auth")]
122    use crate::constants::FAKE_TOKEN;
123
124    use super::*;
125    #[test]
126    fn new_creates_client_correctly() {
127        #[cfg(feature = "auth")]
128        use reqwest::header::HeaderValue;
129        let client = GithubClient::new(
130            #[cfg(feature = "enterprise")]
131            "https://something.com/api/v3",
132            #[cfg(feature = "auth")]
133            FAKE_TOKEN,
134        )
135        .expect("Should build client");
136        #[cfg(feature = "auth")]
137        assert_eq!(client.auth_token, FAKE_TOKEN);
138        #[cfg(feature = "enterprise")]
139        assert_eq!(client.base_url, "https://something.com/api/v3");
140        #[cfg(feature = "auth")]
141        assert_eq!(
142            client.default_headers.get("Authorization"),
143            Some(
144                &format!("token {}", FAKE_TOKEN)
145                    .parse::<HeaderValue>()
146                    .unwrap()
147            )
148        );
149        assert_eq!(
150            client.user_agent,
151            format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
152        );
153    }
154
155    #[test]
156    #[cfg(feature = "auth")]
157    fn setting_auth_token_sets_header() {
158        #[cfg(not(feature = "enterprise"))]
159        let client = GithubClient::new("abc").expect("Should build client");
160        #[cfg(feature = "enterprise")]
161        let client =
162            GithubClient::new("https://abc.abc/api/v3", "abc").expect("Should build client");
163        assert_eq!(
164            client.default_headers.get("Authorization"),
165            Some(&"token abc".parse().unwrap())
166        );
167    }
168
169    #[test]
170    #[cfg(feature = "enterprise")]
171    #[should_panic(expected = "CreationError { kind: BaseUrlWithoutProtocol }")]
172    fn new_errors_on_no_protocol() {
173        GithubClient::new("something", FAKE_TOKEN).expect("Should not work");
174    }
175
176    #[test]
177    #[cfg(feature = "enterprise")]
178    #[should_panic(expected = "CreationError { kind: BaseUrlWithoutApiPath }")]
179    fn new_errors_on_no_api_path() {
180        GithubClient::new("https://something.com", FAKE_TOKEN).unwrap();
181    }
182
183    #[test]
184    #[cfg(feature = "enterprise")]
185    fn new_for_valid_enterprise_works() {
186        GithubClient::new("https://something.com/api/v3", FAKE_TOKEN).unwrap();
187    }
188}