octorust/
lib.rs

1//! A fully generated, opinionated API client library for GitHub.
2//!
3//!
4//! [![docs.rs](https://docs.rs/octorust/badge.svg)](https://docs.rs/octorust)
5//!
6//! ## API Details
7//!
8//! GitHub's v3 REST API.
9//!
10//! [API Terms of Service](https://docs.github.com/articles/github-terms-of-service)
11//!
12//! ### Contact
13//!
14//!
15//! | name | url |
16//! |----|----|
17//! | Support | <https://support.github.com/contact?tags=rest-api> |
18//!
19//! ### License
20//!
21//!
22//! | name | url |
23//! |----|----|
24//! | MIT | <https://spdx.org/licenses/MIT> |
25//!
26//!
27//! ## Client Details
28//!
29//! This client is generated from the [GitHub OpenAPI
30//! specs](https://github.com/github/rest-api-description) based on API spec version `1.1.4`. This way it will remain
31//! up to date as features are added. The documentation for the crate is generated
32//! along with the code to make this library easy to use.
33//!
34//!
35//! To install the library, add the following to your `Cargo.toml` file.
36//!
37//! ```toml
38//! [dependencies]
39//! octorust = "0.10.0"
40//! ```
41//!
42//! ## Basic example
43//!
44//! Typical use will require intializing a `Client`. This requires
45//! a user agent string and set of `auth::Credentials`.
46//!
47//! ```rust
48//! use octorust::{auth::Credentials, Client};
49//!
50//! let github = Client::new(
51//!   String::from("user-agent-name"),
52//!   Credentials::Token(
53//!     String::from("personal-access-token")
54//!   ),
55//! );
56//! ```
57//!
58//! If you are a GitHub enterprise customer, you will want to create a client with the
59//! [Client#host_override](https://docs.rs/octorust/0.10.0/octorust/struct.Client.html#method.host_override) method.
60//!
61//! ## Feature flags
62//!
63//! ### httpcache
64//!
65//! Github supports conditional HTTP requests using etags to checksum responses
66//! Experimental support for utilizing this to cache responses locally with the
67//! `httpcache` feature flag.
68//!
69//! To enable this, add the following to your `Cargo.toml` file:
70//!
71//! ```toml
72//! [dependencies]
73//! octorust = { version = "0.10.0", features = ["httpcache"] }
74//! ```
75//!
76//! Then use the `Client::custom` constructor to provide a cache implementation.
77//!
78//! Here is an example:
79//!
80//! ```rust
81//! use octorust::{auth::Credentials, Client};
82//! #[cfg(feature = "httpcache")]
83//! use octorust::http_cache::HttpCache;
84//!
85//! #[cfg(feature = "httpcache")]
86//! let http_cache = HttpCache::in_home_dir();
87//!
88//! #[cfg(not(feature = "httpcache"))]
89//! let github = Client::custom(
90//!     concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
91//!     Credentials::Token(
92//!       String::from("personal-access-token")
93//!     ),
94//!     reqwest::Client::builder().build().unwrap(),
95//! );
96//!
97//! #[cfg(feature = "httpcache")]
98//! let github = Client::custom(
99//!     concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
100//!     Credentials::Token(
101//!       String::from("personal-access-token")
102//!     ),
103//!     reqwest::Client::builder().build().unwrap(),
104//!     http_cache
105//! );
106//! ```
107//! ## Authenticating GitHub apps
108//!
109//! You can also authenticate via a GitHub app.
110//!
111//! Here is an example:
112//!
113//! ```rust
114//! use std::env;
115//!
116//! use octorust::{Client, auth::{Credentials, InstallationTokenGenerator, JWTCredentials}};
117//! #[cfg(feature = "httpcache")]
118//! use octorust::http_cache::FileBasedCache;
119//! use base64::{Engine, engine::general_purpose::STANDARD};
120//!
121//! let app_id_str = env::var("GH_APP_ID").unwrap();
122//! let app_id = app_id_str.parse::<u64>().unwrap();
123//!
124//! let app_installation_id_str = env::var("GH_INSTALLATION_ID").unwrap();
125//! let app_installation_id = app_installation_id_str.parse::<u64>().unwrap();
126//!
127//! let encoded_private_key = env::var("GH_PRIVATE_KEY").unwrap();
128//! let private_key = STANDARD.decode(encoded_private_key).unwrap();
129//!
130//! // Decode the key.
131//! let key = nom_pem::decode_block(&private_key).unwrap();
132//!
133//! // Get the JWT credentials.
134//! let jwt = JWTCredentials::new(app_id, key.data).unwrap();
135//!
136//! // Create the HTTP cache.
137//! #[cfg(feature = "httpcache")]
138//! let mut dir = dirs::home_dir().expect("Expected a home dir");
139//! #[cfg(feature = "httpcache")]
140//! dir.push(".cache/github");
141//! #[cfg(feature = "httpcache")]
142//! let http_cache = Box::new(FileBasedCache::new(dir));
143//!
144//! let token_generator = InstallationTokenGenerator::new(app_installation_id, jwt);
145//!
146//! #[cfg(not(feature = "httpcache"))]
147//! let github = Client::custom(
148//!     concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
149//!     Credentials::InstallationToken(token_generator),
150//!     reqwest::Client::builder().build().unwrap(),
151//! );
152//!
153//! #[cfg(feature = "httpcache")]
154//! let github = Client::custom(
155//!     concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
156//!     Credentials::InstallationToken(token_generator),
157//!     reqwest::Client::builder().build().unwrap(),
158//!     http_cache,
159//! );
160//! ```
161//!
162//! ## Acknowledgements
163//!
164//! Shout out to [hubcaps](https://github.com/softprops/hubcaps) for paving the
165//! way here. This extends that effort in a generated way so the library is
166//! always up to the date with the OpenAPI spec and no longer requires manual
167//! contributions to add new endpoints.
168//!
169#![allow(clippy::derive_partial_eq_without_eq)]
170#![allow(clippy::too_many_arguments)]
171#![allow(clippy::nonstandard_macro_braces)]
172#![allow(clippy::large_enum_variant)]
173#![allow(clippy::tabs_in_doc_comments)]
174#![allow(missing_docs)]
175#![cfg_attr(docsrs, feature(doc_cfg))]
176
177/// Endpoints to manage GitHub Actions using the REST API.
178pub mod actions;
179/// Activity APIs provide access to notifications, subscriptions, and timelines.
180pub mod activity;
181/// Information for integrations and installations.
182pub mod apps;
183pub mod auth;
184/// Monitor charges and usage from Actions and Packages.
185pub mod billing;
186/// Rich interactions with checks run by your integrations.
187pub mod checks;
188/// Retrieve code scanning alerts from a repository.
189pub mod code_scanning;
190/// Insight into codes of conduct for your communities.
191pub mod codes_of_conduct;
192/// List emojis available to use on GitHub.
193pub mod emojis;
194/// Administer a GitHub enterprise.
195pub mod enterprise_admin;
196/// View, modify your gists.
197pub mod gists;
198/// Raw Git functionality.
199pub mod git;
200/// View gitignore templates.
201pub mod gitignore;
202#[cfg(feature = "httpcache")]
203#[cfg_attr(docsrs, doc(cfg(feature = "httpcache")))]
204pub mod http_cache;
205/// Owner or admin management of users interactions.
206pub mod interactions;
207/// Interact with GitHub Issues.
208pub mod issues;
209/// View various OSS licenses.
210pub mod licenses;
211/// Render Github flavored markdown.
212pub mod markdown;
213/// Endpoints that give information about the API.
214pub mod meta;
215/// Move projects to or from GitHub.
216pub mod migrations;
217/// Manage access of OAuth applications.
218pub mod oauth_authorizations;
219/// Interact with GitHub Orgs.
220pub mod orgs;
221/// Manage packages for authenticated users and organizations.
222pub mod packages;
223/// Interact with GitHub Projects.
224pub mod projects;
225/// Interact with GitHub Pull Requests.
226pub mod pulls;
227/// Check your current rate limit status.
228pub mod rate_limit;
229/// Interact with reactions to various GitHub entities.
230pub mod reactions;
231/// Interact with GitHub Repos.
232pub mod repos;
233/// Provisioning of GitHub organization membership for SCIM-enabled providers.
234pub mod scim;
235/// Look for stuff on GitHub.
236pub mod search;
237/// Retrieve secret scanning alerts from a repository.
238pub mod secret_scanning;
239/// Interact with GitHub Teams.
240pub mod teams;
241pub mod types;
242/// Interact with and view information about users and also current user.
243pub mod users;
244#[doc(hidden)]
245pub mod utils;
246
247pub use reqwest::{header::HeaderMap, StatusCode};
248
249#[derive(Debug)]
250pub struct Response<T> {
251    pub status: reqwest::StatusCode,
252    pub headers: reqwest::header::HeaderMap,
253    pub body: T,
254}
255
256impl<T> Response<T> {
257    pub fn new(status: reqwest::StatusCode, headers: reqwest::header::HeaderMap, body: T) -> Self {
258        Self {
259            status,
260            headers,
261            body,
262        }
263    }
264}
265
266type ClientResult<T> = Result<T, ClientError>;
267
268use thiserror::Error;
269
270/// Errors returned by the client
271#[derive(Debug, Error)]
272pub enum ClientError {
273    // Github only
274    /// Ratelimited
275    #[error("Rate limited for the next {duration} seconds")]
276    RateLimited { duration: u64 },
277    /// JWT errors from auth.rs
278    #[error(transparent)]
279    JsonWebTokenError(#[from] jsonwebtoken::errors::Error),
280    /// IO Errors
281    #[cfg(feature = "httpcache")]
282    #[error(transparent)]
283    #[cfg(feature = "httpcache")]
284    IoError(#[from] std::io::Error),
285    /// URL Parsing Error
286    #[error(transparent)]
287    UrlParserError(#[from] url::ParseError),
288    /// Serde JSON parsing error
289    #[error(transparent)]
290    SerdeJsonError(#[from] serde_json::Error),
291    /// Errors returned by reqwest
292    #[error(transparent)]
293    ReqwestError(#[from] reqwest::Error),
294    /// Errors returned by reqwest::header
295    #[error(transparent)]
296    InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
297    #[cfg(feature = "middleware")]
298    /// Errors returned by reqwest middleware
299    #[error(transparent)]
300    ReqwestMiddleWareError(#[from] reqwest_middleware::Error),
301    /// Generic HTTP Error
302    #[error("HTTP Error. Code: {status}, message: {error}")]
303    HttpError {
304        status: http::StatusCode,
305        headers: reqwest::header::HeaderMap,
306        error: String,
307    },
308}
309
310pub const FALLBACK_HOST: &str = "https://api.github.com";
311
312mod progenitor_support {
313    use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
314
315    const PATH_SET: &AsciiSet = &CONTROLS
316        .add(b' ')
317        .add(b'"')
318        .add(b'#')
319        .add(b'<')
320        .add(b'>')
321        .add(b'?')
322        .add(b'`')
323        .add(b'{')
324        .add(b'}');
325
326    #[allow(dead_code)]
327    pub(crate) fn encode_path(pc: &str) -> String {
328        utf8_percent_encode(pc, PATH_SET).to_string()
329    }
330}
331
332#[derive(Debug, Default)]
333pub(crate) struct Message {
334    pub body: Option<reqwest::Body>,
335    pub content_type: Option<String>,
336}
337
338#[derive(Debug, Default, Clone)]
339pub struct RootDefaultServer {}
340
341impl RootDefaultServer {
342    pub fn default_url(&self) -> &str {
343        "https://api.github.com"
344    }
345}
346
347/// Entrypoint for interacting with the API client.
348#[derive(Clone)]
349pub struct Client {
350    host: String,
351    host_override: Option<String>,
352    agent: String,
353    #[cfg(feature = "middleware")]
354    client: reqwest_middleware::ClientWithMiddleware,
355    #[cfg(not(feature = "middleware"))]
356    client: reqwest::Client,
357    credentials: Option<crate::auth::Credentials>,
358    #[cfg(feature = "httpcache")]
359    http_cache: crate::http_cache::BoxedHttpCache,
360}
361
362impl Client {
363    pub fn new<A, C>(agent: A, credentials: C) -> ClientResult<Self>
364    where
365        A: Into<String>,
366        C: Into<Option<crate::auth::Credentials>>,
367    {
368        let http = reqwest::Client::builder()
369            .redirect(reqwest::redirect::Policy::none())
370            .build()?;
371
372        #[cfg(feature = "middleware")]
373        let client = {
374            let retry_policy =
375                reqwest_retry::policies::ExponentialBackoff::builder().build_with_max_retries(3);
376            reqwest_middleware::ClientBuilder::new(http)
377                // Trace HTTP requests. See the tracing crate to make use of these traces.
378                .with(reqwest_tracing::TracingMiddleware::default())
379                // Retry failed requests.
380                .with(reqwest_conditional_middleware::ConditionalMiddleware::new(
381                    reqwest_retry::RetryTransientMiddleware::new_with_policy(retry_policy),
382                    |req: &reqwest::Request| req.try_clone().is_some(),
383                ))
384                .build()
385        };
386        #[cfg(not(feature = "middleware"))]
387        let client = http;
388
389        #[cfg(feature = "httpcache")]
390        {
391            Ok(Self::custom(
392                agent,
393                credentials,
394                client,
395                <dyn crate::http_cache::HttpCache>::noop(),
396            ))
397        }
398        #[cfg(not(feature = "httpcache"))]
399        {
400            Ok(Self::custom(agent, credentials, client))
401        }
402    }
403
404    #[cfg(feature = "httpcache")]
405    pub fn custom<A, CR>(
406        agent: A,
407        credentials: CR,
408        #[cfg(feature = "middleware")] http: reqwest_middleware::ClientWithMiddleware,
409        #[cfg(not(feature = "middleware"))] http: reqwest::Client,
410        http_cache: crate::http_cache::BoxedHttpCache,
411    ) -> Self
412    where
413        A: Into<String>,
414        CR: Into<Option<crate::auth::Credentials>>,
415    {
416        Self {
417            host: RootDefaultServer::default().default_url().to_string(),
418            host_override: None,
419            agent: agent.into(),
420            client: http,
421            credentials: credentials.into(),
422            http_cache,
423        }
424    }
425
426    #[cfg(not(feature = "httpcache"))]
427    pub fn custom<A, CR>(
428        agent: A,
429        credentials: CR,
430        #[cfg(feature = "middleware")] http: reqwest_middleware::ClientWithMiddleware,
431        #[cfg(not(feature = "middleware"))] http: reqwest::Client,
432    ) -> Self
433    where
434        A: Into<String>,
435        CR: Into<Option<crate::auth::Credentials>>,
436    {
437        Self {
438            host: RootDefaultServer::default().default_url().to_string(),
439            host_override: None,
440            agent: agent.into(),
441            client: http,
442            credentials: credentials.into(),
443        }
444    }
445
446    /// Override the host for all endpoins in the client.
447    pub fn with_host_override<H>(&mut self, host: H) -> &mut Self
448    where
449        H: ToString,
450    {
451        self.host_override = Some(host.to_string());
452        self
453    }
454
455    /// Disables the global host override for the client.
456    pub fn remove_host_override(&mut self) -> &mut Self {
457        self.host_override = None;
458        self
459    }
460
461    pub fn get_host_override(&self) -> Option<&str> {
462        self.host_override.as_deref()
463    }
464
465    pub(crate) fn url(&self, path: &str, host: Option<&str>) -> String {
466        format!(
467            "{}{}",
468            self.get_host_override()
469                .or(host)
470                .unwrap_or(self.host.as_str()),
471            path
472        )
473    }
474
475    pub fn set_credentials<CR>(&mut self, credentials: CR)
476    where
477        CR: Into<Option<crate::auth::Credentials>>,
478    {
479        self.credentials = credentials.into();
480    }
481
482    fn credentials(
483        &self,
484        authentication: crate::auth::AuthenticationConstraint,
485    ) -> Option<&crate::auth::Credentials> {
486        match (authentication, self.credentials.as_ref()) {
487            (crate::auth::AuthenticationConstraint::Unconstrained, creds) => creds,
488            (
489                crate::auth::AuthenticationConstraint::JWT,
490                creds @ Some(&crate::auth::Credentials::JWT(_)),
491            ) => creds,
492            (
493                crate::auth::AuthenticationConstraint::JWT,
494                Some(crate::auth::Credentials::InstallationToken(apptoken)),
495            ) => Some(apptoken.jwt()),
496            (crate::auth::AuthenticationConstraint::JWT, _) => {
497                log::info!(
498                    "Request needs JWT authentication but only a mismatched method is available"
499                );
500                None
501            }
502        }
503    }
504
505    async fn url_and_auth(
506        &self,
507        uri: &str,
508        authentication: crate::auth::AuthenticationConstraint,
509    ) -> ClientResult<(reqwest::Url, Option<String>)> {
510        let mut parsed_url = uri.parse::<reqwest::Url>()?;
511
512        match self.credentials(authentication) {
513            Some(crate::auth::Credentials::Client(id, secret)) => {
514                parsed_url
515                    .query_pairs_mut()
516                    .append_pair("client_id", id)
517                    .append_pair("client_secret", secret);
518                Ok((parsed_url, None))
519            }
520            Some(crate::auth::Credentials::Token(token)) => {
521                let auth = format!("token {}", token);
522                Ok((parsed_url, Some(auth)))
523            }
524            Some(crate::auth::Credentials::JWT(jwt)) => {
525                let auth = format!("Bearer {}", jwt.token());
526                Ok((parsed_url, Some(auth)))
527            }
528            Some(crate::auth::Credentials::InstallationToken(apptoken)) => {
529                let token = if let Some(token) = apptoken.token().await {
530                    token
531                } else {
532                    let mut token_guard = apptoken.access_key.write().await;
533                    if let Some(token) = token_guard.as_ref().and_then(|t| t.token()) {
534                        token.to_owned()
535                    } else {
536                        log::debug!("app token is stale, refreshing");
537
538                        let created_at = tokio::time::Instant::now();
539                        let token = self
540                            .apps()
541                            .create_installation_access_token(
542                                apptoken.installation_id,
543                                &types::AppsCreateInstallationAccessTokenRequest {
544                                    permissions: Default::default(),
545                                    repositories: Default::default(),
546                                    repository_ids: Default::default(),
547                                },
548                            )
549                            .await?;
550                        *token_guard = Some(crate::auth::ExpiringInstallationToken::new(
551                            token.body.token.clone(),
552                            created_at,
553                        ));
554                        token.body.token
555                    }
556                };
557                let auth = format!("token {}", token);
558                Ok((parsed_url, Some(auth)))
559            }
560            None => Ok((parsed_url, None)),
561        }
562    }
563
564    #[cfg(feature = "middleware")]
565    async fn make_request(
566        &self,
567        method: http::Method,
568        uri: &str,
569        message: Message,
570        media_type: crate::utils::MediaType,
571        authentication: crate::auth::AuthenticationConstraint,
572    ) -> ClientResult<reqwest_middleware::RequestBuilder> {
573        let (url, auth) = self.url_and_auth(uri, authentication).await?;
574
575        let mut req = self.client.request(method, url);
576
577        if let Some(content_type) = &message.content_type {
578            req = req.header(http::header::CONTENT_TYPE, content_type.clone());
579        }
580
581        req = req.header(http::header::USER_AGENT, &*self.agent);
582        req = req.header(http::header::ACCEPT, &media_type.to_string());
583
584        if let Some(auth_str) = auth {
585            req = req.header(http::header::AUTHORIZATION, &*auth_str);
586        }
587
588        if let Some(body) = message.body {
589            req = req.body(body);
590        }
591
592        Ok(req)
593    }
594    #[cfg(not(feature = "middleware"))]
595    async fn make_request(
596        &self,
597        method: http::Method,
598        uri: &str,
599        message: Message,
600        media_type: crate::utils::MediaType,
601        authentication: crate::auth::AuthenticationConstraint,
602    ) -> ClientResult<reqwest::RequestBuilder> {
603        let (url, auth) = self.url_and_auth(uri, authentication).await?;
604
605        let mut req = self.client.request(method, url);
606
607        if let Some(content_type) = &message.content_type {
608            req = req.header(http::header::CONTENT_TYPE, content_type.clone());
609        }
610
611        req = req.header(http::header::USER_AGENT, &*self.agent);
612        req = req.header(http::header::ACCEPT, &media_type.to_string());
613
614        if let Some(auth_str) = auth {
615            req = req.header(http::header::AUTHORIZATION, &*auth_str);
616        }
617
618        if let Some(body) = message.body {
619            req = req.body(body);
620        }
621
622        Ok(req)
623    }
624
625    async fn request<Out>(
626        &self,
627        method: http::Method,
628        uri: &str,
629        message: Message,
630        media_type: crate::utils::MediaType,
631        authentication: crate::auth::AuthenticationConstraint,
632    ) -> ClientResult<(Option<crate::utils::NextLink>, crate::Response<Out>)>
633    where
634        Out: serde::de::DeserializeOwned + 'static + Send,
635    {
636        #[cfg(not(feature = "httpcache"))]
637        let req = self
638            .make_request(method.clone(), uri, message, media_type, authentication)
639            .await?;
640
641        #[cfg(feature = "httpcache")]
642        let req = {
643            let mut req = self
644                .make_request(method.clone(), uri, message, media_type, authentication)
645                .await?;
646
647            if method == http::Method::GET {
648                if let Ok(etag) = self.http_cache.lookup_etag(&uri) {
649                    req = req.header(http::header::IF_NONE_MATCH, etag);
650                }
651            }
652
653            req
654        };
655
656        let response = req.send().await?;
657
658        #[cfg(not(feature = "httpcache"))]
659        let (remaining, reset) = crate::utils::get_header_values(response.headers());
660
661        #[cfg(feature = "httpcache")]
662        let (remaining, reset, etag) = crate::utils::get_header_values(response.headers());
663
664        let status = response.status();
665        let headers = response.headers().clone();
666        let link = response
667            .headers()
668            .get(http::header::LINK)
669            .and_then(|l| l.to_str().ok())
670            .and_then(|l| parse_link_header::parse(l).ok());
671        let next_link = link.as_ref().and_then(crate::utils::next_link);
672
673        let response_body = response.bytes().await?;
674
675        if status.is_success() {
676            log::debug!("Received successful response. Read payload.");
677            #[cfg(feature = "httpcache")]
678            {
679                if let Some(etag) = etag {
680                    if let Err(e) = self.http_cache.cache_response(
681                        &uri,
682                        &response_body,
683                        &etag,
684                        &next_link.as_ref().map(|n| n.0.clone()),
685                    ) {
686                        // failing to cache isn't fatal, so just log & swallow the error
687                        log::info!("failed to cache body & etag: {}", e);
688                    }
689                }
690            }
691
692            let parsed_response = if status == http::StatusCode::NO_CONTENT
693                || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
694            {
695                serde_json::from_str("null")?
696            } else {
697                serde_json::from_slice::<Out>(&response_body)?
698            };
699            Ok((
700                next_link,
701                crate::Response::new(status, headers, parsed_response),
702            ))
703        } else if status.is_redirection() {
704            match status {
705                http::StatusCode::NOT_MODIFIED => {
706                    // only supported case is when client provides if-none-match
707                    // header when cargo builds with --cfg feature="httpcache"
708                    #[cfg(feature = "httpcache")]
709                    {
710                        let body = self.http_cache.lookup_body(&uri).unwrap();
711                        let out = serde_json::from_str::<Out>(&body).unwrap();
712                        let link = match next_link {
713                            Some(next_link) => Ok(Some(next_link)),
714                            None => self
715                                .http_cache
716                                .lookup_next_link(&uri)
717                                .map(|next_link| next_link.map(crate::utils::NextLink)),
718                        };
719                        link.map(|link| (link, Response::new(status, headers, out)))
720                    }
721                    #[cfg(not(feature = "httpcache"))]
722                    {
723                        unreachable!(
724                            "this should not be reachable without the httpcache feature enabled"
725                        )
726                    }
727                }
728                _ => {
729                    // The body still needs to be parsed. Except in the case of 304 (handled above),
730                    // returning a body in the response is allowed.
731                    let body = if std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>() {
732                        serde_json::from_str("null")?
733                    } else {
734                        serde_json::from_slice::<Out>(&response_body)?
735                    };
736
737                    Ok((None, crate::Response::new(status, headers, body)))
738                }
739            }
740        } else {
741            let error = match (remaining, reset) {
742                (Some(remaining), Some(reset)) if remaining == 0 => {
743                    let now = std::time::SystemTime::now()
744                        .duration_since(std::time::UNIX_EPOCH)
745                        .unwrap()
746                        .as_secs();
747                    ClientError::RateLimited {
748                        duration: u64::from(reset).saturating_sub(now),
749                    }
750                }
751                _ => {
752                    if response_body.is_empty() {
753                        ClientError::HttpError {
754                            status,
755                            headers,
756                            error: "empty response".into(),
757                        }
758                    } else {
759                        ClientError::HttpError {
760                            status,
761                            headers,
762                            error: String::from_utf8_lossy(&response_body).into(),
763                        }
764                    }
765                }
766            };
767            Err(error)
768        }
769    }
770
771    async fn request_entity<D>(
772        &self,
773        method: http::Method,
774        uri: &str,
775        message: Message,
776        media_type: crate::utils::MediaType,
777        authentication: crate::auth::AuthenticationConstraint,
778    ) -> ClientResult<crate::Response<D>>
779    where
780        D: serde::de::DeserializeOwned + 'static + Send,
781    {
782        let (_, r) = self
783            .request(method, uri, message, media_type, authentication)
784            .await?;
785        Ok(r)
786    }
787
788    async fn get<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
789    where
790        D: serde::de::DeserializeOwned + 'static + Send,
791    {
792        self.get_media(uri, crate::utils::MediaType::Json, message)
793            .await
794    }
795
796    async fn get_media<D>(
797        &self,
798        uri: &str,
799        media: crate::utils::MediaType,
800        message: Message,
801    ) -> ClientResult<crate::Response<D>>
802    where
803        D: serde::de::DeserializeOwned + 'static + Send,
804    {
805        self.request_entity(
806            http::Method::GET,
807            uri,
808            message,
809            media,
810            crate::auth::AuthenticationConstraint::Unconstrained,
811        )
812        .await
813    }
814
815    async fn get_all_pages<D>(
816        &self,
817        uri: &str,
818        _message: Message,
819    ) -> ClientResult<crate::Response<Vec<D>>>
820    where
821        D: serde::de::DeserializeOwned + 'static + Send,
822    {
823        self.unfold(uri).await
824    }
825
826    async fn get_pages<D>(
827        &self,
828        uri: &str,
829    ) -> ClientResult<(Option<crate::utils::NextLink>, crate::Response<Vec<D>>)>
830    where
831        D: serde::de::DeserializeOwned + 'static + Send,
832    {
833        self.request(
834            http::Method::GET,
835            uri,
836            Message::default(),
837            crate::utils::MediaType::Json,
838            crate::auth::AuthenticationConstraint::Unconstrained,
839        )
840        .await
841    }
842
843    async fn get_pages_url<D>(
844        &self,
845        url: &reqwest::Url,
846    ) -> ClientResult<(Option<crate::utils::NextLink>, crate::Response<Vec<D>>)>
847    where
848        D: serde::de::DeserializeOwned + 'static + Send,
849    {
850        self.request(
851            http::Method::GET,
852            url.as_str(),
853            Message::default(),
854            crate::utils::MediaType::Json,
855            crate::auth::AuthenticationConstraint::Unconstrained,
856        )
857        .await
858    }
859
860    async fn post<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
861    where
862        D: serde::de::DeserializeOwned + 'static + Send,
863    {
864        self.post_media(
865            uri,
866            message,
867            crate::utils::MediaType::Json,
868            crate::auth::AuthenticationConstraint::Unconstrained,
869        )
870        .await
871    }
872
873    async fn post_media<D>(
874        &self,
875        uri: &str,
876        message: Message,
877        media: crate::utils::MediaType,
878        authentication: crate::auth::AuthenticationConstraint,
879    ) -> ClientResult<crate::Response<D>>
880    where
881        D: serde::de::DeserializeOwned + 'static + Send,
882    {
883        self.request_entity(http::Method::POST, uri, message, media, authentication)
884            .await
885    }
886
887    async fn patch_media<D>(
888        &self,
889        uri: &str,
890        message: Message,
891        media: crate::utils::MediaType,
892    ) -> ClientResult<crate::Response<D>>
893    where
894        D: serde::de::DeserializeOwned + 'static + Send,
895    {
896        self.request_entity(
897            http::Method::PATCH,
898            uri,
899            message,
900            media,
901            crate::auth::AuthenticationConstraint::Unconstrained,
902        )
903        .await
904    }
905
906    async fn patch<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
907    where
908        D: serde::de::DeserializeOwned + 'static + Send,
909    {
910        self.patch_media(uri, message, crate::utils::MediaType::Json)
911            .await
912    }
913
914    async fn put<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
915    where
916        D: serde::de::DeserializeOwned + 'static + Send,
917    {
918        self.put_media(uri, message, crate::utils::MediaType::Json)
919            .await
920    }
921
922    async fn put_media<D>(
923        &self,
924        uri: &str,
925        message: Message,
926        media: crate::utils::MediaType,
927    ) -> ClientResult<crate::Response<D>>
928    where
929        D: serde::de::DeserializeOwned + 'static + Send,
930    {
931        self.request_entity(
932            http::Method::PUT,
933            uri,
934            message,
935            media,
936            crate::auth::AuthenticationConstraint::Unconstrained,
937        )
938        .await
939    }
940
941    async fn delete<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
942    where
943        D: serde::de::DeserializeOwned + 'static + Send,
944    {
945        self.request_entity(
946            http::Method::DELETE,
947            uri,
948            message,
949            crate::utils::MediaType::Json,
950            crate::auth::AuthenticationConstraint::Unconstrained,
951        )
952        .await
953    }
954
955    /// "unfold" paginated results of a vector of items
956    async fn unfold<D>(&self, uri: &str) -> ClientResult<crate::Response<Vec<D>>>
957    where
958        D: serde::de::DeserializeOwned + 'static + Send,
959    {
960        let mut global_items = Vec::new();
961        let (new_link, mut response) = self.get_pages(uri).await?;
962        let mut link = new_link;
963        while !response.body.is_empty() {
964            global_items.append(&mut response.body);
965            // We need to get the next link.
966            if let Some(url) = &link {
967                let url = reqwest::Url::parse(&url.0)?;
968                let (new_link, new_response) = self.get_pages_url(&url).await?;
969                link = new_link;
970                response = new_response;
971            }
972        }
973
974        Ok(Response::new(
975            response.status,
976            response.headers,
977            global_items,
978        ))
979    }
980
981    /// Endpoints to manage GitHub Actions using the REST API.
982    pub fn actions(&self) -> actions::Actions {
983        actions::Actions::new(self.clone())
984    }
985
986    /// Activity APIs provide access to notifications, subscriptions, and timelines.
987    pub fn activity(&self) -> activity::Activity {
988        activity::Activity::new(self.clone())
989    }
990
991    /// Information for integrations and installations.
992    pub fn apps(&self) -> apps::Apps {
993        apps::Apps::new(self.clone())
994    }
995
996    /// Monitor charges and usage from Actions and Packages.
997    pub fn billing(&self) -> billing::Billing {
998        billing::Billing::new(self.clone())
999    }
1000
1001    /// Rich interactions with checks run by your integrations.
1002    pub fn checks(&self) -> checks::Checks {
1003        checks::Checks::new(self.clone())
1004    }
1005
1006    /// Retrieve code scanning alerts from a repository.
1007    pub fn code_scanning(&self) -> code_scanning::CodeScanning {
1008        code_scanning::CodeScanning::new(self.clone())
1009    }
1010
1011    /// Insight into codes of conduct for your communities.
1012    pub fn codes_of_conduct(&self) -> codes_of_conduct::CodesOfConduct {
1013        codes_of_conduct::CodesOfConduct::new(self.clone())
1014    }
1015
1016    /// List emojis available to use on GitHub.
1017    pub fn emojis(&self) -> emojis::Emojis {
1018        emojis::Emojis::new(self.clone())
1019    }
1020
1021    /// Administer a GitHub enterprise.
1022    pub fn enterprise_admin(&self) -> enterprise_admin::EnterpriseAdmin {
1023        enterprise_admin::EnterpriseAdmin::new(self.clone())
1024    }
1025
1026    /// View, modify your gists.
1027    pub fn gists(&self) -> gists::Gists {
1028        gists::Gists::new(self.clone())
1029    }
1030
1031    /// Raw Git functionality.
1032    pub fn git(&self) -> git::Git {
1033        git::Git::new(self.clone())
1034    }
1035
1036    /// View gitignore templates.
1037    pub fn gitignore(&self) -> gitignore::Gitignore {
1038        gitignore::Gitignore::new(self.clone())
1039    }
1040
1041    /// Owner or admin management of users interactions.
1042    pub fn interactions(&self) -> interactions::Interactions {
1043        interactions::Interactions::new(self.clone())
1044    }
1045
1046    /// Interact with GitHub Issues.
1047    pub fn issues(&self) -> issues::Issues {
1048        issues::Issues::new(self.clone())
1049    }
1050
1051    /// View various OSS licenses.
1052    pub fn licenses(&self) -> licenses::Licenses {
1053        licenses::Licenses::new(self.clone())
1054    }
1055
1056    /// Render Github flavored markdown.
1057    pub fn markdown(&self) -> markdown::Markdown {
1058        markdown::Markdown::new(self.clone())
1059    }
1060
1061    /// Endpoints that give information about the API.
1062    pub fn meta(&self) -> meta::Meta {
1063        meta::Meta::new(self.clone())
1064    }
1065
1066    /// Move projects to or from GitHub.
1067    pub fn migrations(&self) -> migrations::Migrations {
1068        migrations::Migrations::new(self.clone())
1069    }
1070
1071    /// Manage access of OAuth applications.
1072    pub fn oauth_authorizations(&self) -> oauth_authorizations::OauthAuthorizations {
1073        oauth_authorizations::OauthAuthorizations::new(self.clone())
1074    }
1075
1076    /// Interact with GitHub Orgs.
1077    pub fn orgs(&self) -> orgs::Orgs {
1078        orgs::Orgs::new(self.clone())
1079    }
1080
1081    /// Manage packages for authenticated users and organizations.
1082    pub fn packages(&self) -> packages::Packages {
1083        packages::Packages::new(self.clone())
1084    }
1085
1086    /// Interact with GitHub Projects.
1087    pub fn projects(&self) -> projects::Projects {
1088        projects::Projects::new(self.clone())
1089    }
1090
1091    /// Interact with GitHub Pull Requests.
1092    pub fn pulls(&self) -> pulls::Pulls {
1093        pulls::Pulls::new(self.clone())
1094    }
1095
1096    /// Check your current rate limit status.
1097    pub fn rate_limit(&self) -> rate_limit::RateLimit {
1098        rate_limit::RateLimit::new(self.clone())
1099    }
1100
1101    /// Interact with reactions to various GitHub entities.
1102    pub fn reactions(&self) -> reactions::Reactions {
1103        reactions::Reactions::new(self.clone())
1104    }
1105
1106    /// Interact with GitHub Repos.
1107    pub fn repos(&self) -> repos::Repos {
1108        repos::Repos::new(self.clone())
1109    }
1110
1111    /// Provisioning of GitHub organization membership for SCIM-enabled providers.
1112    pub fn scim(&self) -> scim::Scim {
1113        scim::Scim::new(self.clone())
1114    }
1115
1116    /// Look for stuff on GitHub.
1117    pub fn search(&self) -> search::Search {
1118        search::Search::new(self.clone())
1119    }
1120
1121    /// Retrieve secret scanning alerts from a repository.
1122    pub fn secret_scanning(&self) -> secret_scanning::SecretScanning {
1123        secret_scanning::SecretScanning::new(self.clone())
1124    }
1125
1126    /// Interact with GitHub Teams.
1127    pub fn teams(&self) -> teams::Teams {
1128        teams::Teams::new(self.clone())
1129    }
1130
1131    /// Interact with and view information about users and also current user.
1132    pub fn users(&self) -> users::Users {
1133        users::Users::new(self.clone())
1134    }
1135}