Skip to main content

octocrab/
lib.rs

1//! # Octocrab: A modern, extensible GitHub API client.
2//! Octocrab is an third party GitHub API client, allowing you to easily build
3//! your own GitHub integrations or bots. `octocrab` comes with two primary
4//! set of APIs for communicating with GitHub, a high level strongly typed
5//! semantic API, and a lower level HTTP API for extending behaviour.
6//!
7//! ## Semantic API
8//! The semantic API provides strong typing around GitHub's API, as well as a
9//! set of [`models`] that maps to GitHub's types. Currently the following
10//! modules are available.
11//!
12//! - [`actions`] GitHub Actions
13//! - [`activity`] GitHub Activity
14//! - [`apps`] GitHub Apps
15//! - [`checks`] GitHub Checks
16//! - [`code_scannings`] Code Scanning
17//! - [`commits`] GitHub Commits
18//! - [`current`] Information about the current user.
19//! - [`events`] GitHub Events
20//! - [`gists`] Gists
21//! - [`gitignore`] Gitignore templates
22//! - [`Octocrab::graphql`] GraphQL.
23//! - [`issues`] Issues and related items, e.g. comments, labels, etc.
24//! - [`licenses`] License Metadata.
25//! - [`markdown`] Rendering Markdown with GitHub
26//! - [`orgs`] GitHub Organisations
27//! - [`projects`] GitHub Projects
28//! - [`pulls`] Pull Requests
29//! - [`ratelimit`] Rate Limiting
30//! - [`repos`] Repositories
31//! - [`repos::forks`] Repository forks
32//! - [`repos::releases`] Repository releases
33//! - [`search`] Using GitHub's search.
34//! - [`teams`] Teams
35//! - [`users`] Users
36//! - [`classroom`] GitHub Classroom
37//! - [`workflows`] GitHub Workflows
38//!
39//! #### Getting a Pull Request
40//! ```no_run
41//! # async fn run() -> octocrab::Result<()> {
42//! // Get pull request #404 from `octocrab/repo`.
43//! let pr = octocrab::instance().pulls("octocrab", "repo").get(404).await?;
44//! # Ok(())
45//! # }
46//! ```
47//!
48//! All methods with multiple optional parameters are built as `Builder`
49//! structs, allowing you to easily specify parameters.
50//!
51//! #### Listing issues
52//! ```no_run
53//! # async fn run() -> octocrab::Result<()> {
54//! use octocrab::{models, params};
55//!
56//! let octocrab = octocrab::instance();
57//! // Returns the first page of all issues.
58//! let mut page = octocrab.issues("octocrab", "repo")
59//!     .list()
60//!     // Optional Parameters
61//!     .creator("octocrab")
62//!     .state(params::State::All)
63//!     .per_page(50)
64//!     .send()
65//!     .await?;
66//!
67//! // Go through every page of issues. Warning: There's no rate limiting so
68//! // be careful.
69//! let results = octocrab.all_pages::<models::issues::Issue>(page).await?;
70//!
71//! # Ok(())
72//! # }
73//! ```
74//!
75//! ## HTTP API
76//! The typed API currently doesn't cover all of GitHub's API at this time, and
77//! even if it did GitHub is in active development and this library will
78//! likely always be somewhat behind GitHub at some points in time. However that
79//! shouldn't mean that in order to use those features that you have to now fork
80//! or replace `octocrab` with your own solution.
81//!
82//! Instead `octocrab` exposes a suite of HTTP methods allowing you to easily
83//! extend `Octocrab`'s existing behaviour. Using these HTTP methods allows you
84//! to keep using the same authentication and configuration, while having
85//! control over the request and response. There is a method for each HTTP
86//! method `get`, `post`, `patch`, `put`, `delete`, all of which accept a
87//! relative route and a optional body.
88//!
89//! ```no_run
90//! # async fn run() -> octocrab::Result<()> {
91//! let user: octocrab::models::Author = octocrab::instance()
92//!     .get("/user", None::<&()>)
93//!     .await?;
94//! # Ok(())
95//! # }
96//! ```
97//!
98//! Each of the HTTP methods expects a body, formats the URL with the base
99//! URL, and errors if GitHub doesn't return a successful status, but this isn't
100//! always desired when working with GitHub's API, sometimes you need to check
101//! the response status or headers. As such there are companion methods `_get`,
102//! `_post`, etc. that perform no additional pre or post-processing to
103//! the request.
104//!
105//! ```no_run
106//! # use http::Uri;
107//! # async fn run() -> octocrab::Result<()> {
108//! let octocrab = octocrab::instance();
109//! let response = octocrab
110//!     ._get("https://api.github.com/organizations")
111//!     .await?;
112//!
113//! // You can also use `Uri::builder().authority("<my custom base>").path_and_query("<my custom path>")` if you want to customize the base uri and path.
114//! let response =  octocrab
115//!     ._get(Uri::builder().path_and_query("/organizations").build().expect("valid uri"))
116//!     .await?;
117//! # Ok(())
118//! # }
119//! ```
120//!
121//! You can use the those HTTP methods to easily create your own extensions to
122//! `Octocrab`'s typed API. (Requires `async_trait`).
123//! ```
124//! use octocrab::{Octocrab, Page, Result, models};
125//!
126//! #[async_trait::async_trait]
127//! trait OrganisationExt {
128//!   async fn list_every_organisation(&self) -> Result<Page<models::orgs::Organization>>;
129//! }
130//!
131//! #[async_trait::async_trait]
132//! impl OrganisationExt for Octocrab {
133//!   async fn list_every_organisation(&self) -> Result<Page<models::orgs::Organization>> {
134//!     self.get("organizations", None::<&()>).await
135//!   }
136//! }
137//! ```
138//!
139//! You can also easily access new properties that aren't available in the
140//! current models using `serde`.
141//!
142//! ```no_run
143//! use serde::Deserialize;
144//!
145//! #[derive(Deserialize)]
146//! struct RepositoryWithVisibility {
147//!     #[serde(flatten)]
148//!     inner: octocrab::models::Repository,
149//!     visibility: String,
150//! }
151//!
152//!
153//! # async fn run() -> octocrab::Result<()> {
154//! let my_repo = octocrab::instance()
155//!     .get::<RepositoryWithVisibility, _, _>("https://api.github.com/repos/XAMPPRocky/octocrab", None::<&()>)
156//!     .await?;
157//! # Ok(())
158//! # }
159//! ```
160//!
161//!
162//!
163//! ## Static API
164//! `octocrab` also provides a statically reference count version of its API,
165//! allowing you to easily plug it into existing systems without worrying
166//! about having to integrate and pass around the client.
167//!
168//! ```
169//! // Initialises the static instance with your configuration and returns an
170//! // instance of the client.
171//! # use octocrab::Octocrab;
172//! tokio_test::block_on(async {
173//! octocrab::initialise(Octocrab::default());
174//! // Gets a instance of `Octocrab` from the static API. If you call this
175//! // without first calling `octocrab::initialise` a default client will be
176//! // initialised and returned instead.
177//! octocrab::instance();
178//! # })
179//! ```
180//!
181//! ## GitHub webhook application support
182//!
183//! `octocrab` provides [deserializable datatypes](crate::models::webhook_events)
184//! for the payloads received by a GitHub application [responding to
185//! webhooks](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/building-a-github-app-that-responds-to-webhook-events).
186//! This allows you to write a typesafe application using Rust with
187//! pattern-matching/enum-dispatch to respond to events.
188//!
189//! **Note**: Webhook support in `octocrab` is still beta, not all known webhook events are
190//! strongly typed.
191//!
192//! ```no_run
193//! # use http::request::Request;
194//! # use tracing::{warn, info};
195//! # use octocrab::models::webhook_events::*;
196//! # let request_from_github = Request::post("https://my-webhook-url.com").body(vec![0_u8]).unwrap();
197//! // request_from_github is the HTTP request your webhook handler received
198//! let (parts, body) = request_from_github.into_parts();
199//! let header = parts.headers.get("X-GitHub-Event").unwrap().to_str().unwrap();
200//!
201//! let event = WebhookEvent::try_from_header_and_body(header, &body).unwrap();
202//! // Now you can match on event type and call any specific handling logic
203//! match event.kind {
204//!     WebhookEventType::Ping => info!("Received a ping"),
205//!     WebhookEventType::PullRequest => info!("Received a pull request event"),
206//!     // ...
207//!     _ => warn!("Ignored event"),
208//! };
209//! ```
210#![cfg_attr(test, recursion_limit = "512")]
211#![cfg_attr(docsrs, feature(doc_cfg))]
212
213mod api;
214mod body;
215mod error;
216mod from_response;
217mod page;
218
219pub mod auth;
220pub mod etag;
221pub mod models;
222pub mod params;
223pub mod service;
224
225use api::repos::RepoRef;
226use api::users::UserRef;
227pub use body::OctoBody;
228use chrono::{DateTime, Utc};
229use http::{HeaderMap, HeaderValue, Method, Uri};
230use http_body_util::combinators::BoxBody;
231use http_body_util::BodyExt;
232use service::middleware::auth_header::AuthHeaderLayer;
233use service::middleware::cache::{CacheStorage, HttpCacheLayer};
234use std::convert::{Infallible, TryInto};
235use std::fmt;
236use std::future::Future;
237use std::io::Write;
238use std::marker::PhantomData;
239use std::pin::Pin;
240use std::str::FromStr;
241use std::sync::{Arc, RwLock};
242use web_time::Duration;
243
244use http::{header::HeaderName, StatusCode};
245use hyper::{Request, Response};
246
247use secrecy::{ExposeSecret, SecretString};
248use serde::{Deserialize, Serialize};
249use snafu::*;
250use tower::{buffer::Buffer, util::BoxService, BoxError, Layer, Service, ServiceExt};
251
252use bytes::Bytes;
253use http::header::USER_AGENT;
254use http::request::Builder;
255#[cfg(feature = "opentls")]
256use hyper_tls::HttpsConnector;
257
258#[cfg(feature = "rustls")]
259use hyper_rustls::HttpsConnectorBuilder;
260
261#[cfg(feature = "retry")]
262use tower::retry::{Retry, RetryLayer};
263
264#[cfg(feature = "timeout")]
265use hyper_timeout::TimeoutConnector;
266
267use tower_http::{classify::ServerErrorsFailureClass, map_response_body::MapResponseBodyLayer};
268
269#[cfg(feature = "tracing")]
270use {tower_http::trace::TraceLayer, tracing::Span};
271
272use crate::api::codes_of_conduct;
273use crate::error::{
274    HttpSnafu, HyperSnafu, InvalidUtf8Snafu, SerdeSnafu, SerdeUrlEncodedSnafu, ServiceSnafu,
275    UriParseError, UriParseSnafu, UriSnafu,
276};
277
278use crate::service::middleware::base_uri::BaseUriLayer;
279use crate::service::middleware::extra_headers::ExtraHeadersLayer;
280
281#[cfg(feature = "retry")]
282use crate::service::middleware::retry::RetryConfig;
283
284use auth::{AppAuth, Auth};
285use models::{AppId, InstallationId, InstallationToken, RepositoryId, UserId};
286
287pub use self::{
288    api::{
289        actions, activity, apps, checks, classroom, code_scannings, commits, current, events,
290        gists, gitignore, hooks, issues, licenses, markdown, orgs, projects, pulls, ratelimit,
291        repos, search, teams, users, workflows,
292    },
293    error::{Error, GitHubError},
294    from_response::FromResponse,
295    page::Page,
296};
297
298#[cfg(all(feature = "jwt-rust-crypto", feature = "jwt-aws-lc-rs"))]
299compile_error!(
300    "feature \"jwt-rust-crypto\" and feature \"jwt-aws-lc-rs\" cannot be enabled at the same time"
301);
302
303#[cfg(not(any(feature = "jwt-rust-crypto", feature = "jwt-aws-lc-rs")))]
304compile_error!("at least one of the features \"jwt-rust-crypto\" and feature \"jwt-aws-lc-rs\" must be enabled");
305
306/// A convenience type with a default error type of [`Error`].
307pub type Result<T, E = error::Error> = std::result::Result<T, E>;
308
309const GITHUB_BASE_URI: &str = "https://api.github.com";
310const GITHUB_BASE_UPLOAD_URI: &str = "https://uploads.github.com";
311
312// This `include!` gives us pub const _SET_HEADERS_MAP: [(&str, &str)]
313// generated from Cargo.toml `[package.metadata.github-api].request-headers` array, like
314// ```
315// [package.metadata.github-api]
316//
317// request-headers = ["X-GitHub-Api-Version: 2022-11-28", ]
318// ```
319include!(concat!(env!("OUT_DIR"), "/headers_metadata.rs"));
320
321#[cfg(feature = "default-client")]
322static STATIC_INSTANCE: std::sync::LazyLock<arc_swap::ArcSwap<Octocrab>> =
323    std::sync::LazyLock::new(|| arc_swap::ArcSwap::from_pointee(Octocrab::default()));
324
325/// Formats a GitHub preview from it's name into the full value for the
326/// `Accept` header.
327/// ```
328/// assert_eq!(octocrab::format_preview("machine-man"), "application/vnd.github.machine-man-preview");
329/// ```
330pub fn format_preview(preview: impl AsRef<str>) -> String {
331    format!("application/vnd.github.{}-preview", preview.as_ref())
332}
333
334/// Formats a media type from it's name into the full value for the
335/// `Accept` header.
336/// ```
337/// assert_eq!(octocrab::format_media_type("html"), "application/vnd.github.v3.html+json");
338/// assert_eq!(octocrab::format_media_type("json"), "application/vnd.github.v3.json");
339/// assert_eq!(octocrab::format_media_type("patch"), "application/vnd.github.v3.patch");
340/// ```
341pub fn format_media_type(media_type: impl AsRef<str>) -> String {
342    let media_type = media_type.as_ref();
343    let json_suffix = match media_type {
344        "raw" | "text" | "html" | "full" => "+json",
345        _ => "",
346    };
347
348    format!("application/vnd.github.v3.{media_type}{json_suffix}")
349}
350
351#[derive(Debug, Deserialize)]
352struct GitHubErrorBody {
353    pub documentation_url: Option<String>,
354    pub errors: Option<Vec<serde_json::Value>>,
355    pub message: String,
356}
357
358/// Maps a GitHub error response into and `Err()` variant if the status is
359/// not a success.
360pub async fn map_github_error(
361    response: http::Response<BoxBody<Bytes, crate::Error>>,
362) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
363    if response.status().is_success() {
364        Ok(response)
365    } else {
366        let (parts, body) = response.into_parts();
367        let GitHubErrorBody {
368            documentation_url,
369            errors,
370            message,
371        } = serde_json::from_slice(body.collect().await?.to_bytes().as_ref())
372            .context(error::SerdeSnafu)?;
373
374        Err(error::Error::GitHub {
375            source: Box::new(GitHubError {
376                status_code: parts.status,
377                documentation_url,
378                errors,
379                message,
380            }),
381            backtrace: Backtrace::capture(),
382        })
383    }
384}
385
386/// Initialises the static instance using the configuration set by
387/// `builder`.
388/// ```
389/// # #[tokio::main]
390/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
391/// let octocrab = octocrab::initialise(octocrab::Octocrab::default());
392/// # Ok(())
393/// # }
394/// ```
395#[cfg(feature = "default-client")]
396#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
397pub fn initialise(crab: Octocrab) -> Arc<Octocrab> {
398    STATIC_INSTANCE.swap(Arc::from(crab))
399}
400
401/// Returns a new instance of [`Octocrab`]. If it hasn't been previously
402/// initialised it returns a default instance with no authentication set.
403/// ```
404/// #[tokio::main]
405/// async fn main() -> () {
406/// let octocrab = octocrab::instance();
407/// }
408/// ```
409#[cfg(feature = "default-client")]
410#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
411pub fn instance() -> Arc<Octocrab> {
412    STATIC_INSTANCE.load().clone()
413}
414
415type Executor = Box<dyn Fn(Pin<Box<dyn Future<Output = ()>>>)>;
416
417/// A builder struct for `Octocrab`, allowing you to configure the client, such
418/// as using GitHub previews, the github instance, authentication, etc.
419///
420/// ```
421/// # #[tokio::main]
422/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
423/// let octocrab = octocrab::OctocrabBuilder::default()
424///     .add_preview("machine-man")
425///     .base_uri("https://github.example.com")?
426///     .build()?;
427/// # Ok(())
428/// # }
429/// ```
430///
431/// OctocrabBuilder can be extended with a custom config, see [DefaultOctocrabBuilderConfig] for an example
432pub struct OctocrabBuilder<Svc, Config, Auth, LayerReady> {
433    service: Svc,
434    auth: Auth,
435    config: Config,
436    _layer_ready: PhantomData<LayerReady>,
437    executor: Option<Executor>,
438}
439
440//Indicates weather the builder supports config
441pub struct NoConfig {}
442
443//Indicates weather the builder supports service that is already inside builder
444pub struct NoSvc {}
445
446//Indicates weather builder supports with_layer(This is somewhat redundant given NoSvc exists, but we have to use this until specialization is stable)
447pub struct NotLayerReady {}
448pub struct LayerReady {}
449
450//Indicates weather the builder supports auth
451pub struct NoAuth {}
452
453impl OctocrabBuilder<NoSvc, NoConfig, NoAuth, NotLayerReady> {
454    pub fn new_empty() -> Self {
455        OctocrabBuilder {
456            service: NoSvc {},
457            auth: NoAuth {},
458            config: NoConfig {},
459            _layer_ready: PhantomData,
460            executor: None,
461        }
462    }
463}
464
465impl OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
466    pub fn new() -> Self {
467        OctocrabBuilder::default()
468    }
469}
470
471impl<Config, Auth> OctocrabBuilder<NoSvc, Config, Auth, NotLayerReady> {
472    pub fn with_service<Svc>(self, service: Svc) -> OctocrabBuilder<Svc, Config, Auth, LayerReady> {
473        OctocrabBuilder {
474            service,
475            auth: self.auth,
476            config: self.config,
477            _layer_ready: PhantomData,
478            executor: None,
479        }
480    }
481}
482
483impl<Svc, Config, Auth, B> OctocrabBuilder<Svc, Config, Auth, LayerReady>
484where
485    Svc: Service<Request<OctoBody>, Response = Response<B>> + Send + 'static,
486    Svc::Future: Send + 'static,
487    Svc::Error: Into<BoxError>,
488    B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
489    B::Error: Into<BoxError>,
490{
491    pub fn with_executor(
492        self,
493        executor: Executor,
494    ) -> OctocrabBuilder<Svc, Config, Auth, LayerReady> {
495        OctocrabBuilder {
496            service: self.service,
497            auth: self.auth,
498            config: self.config,
499            _layer_ready: PhantomData,
500            executor: Some(executor),
501        }
502    }
503}
504
505impl<Svc, Config, Auth, B> OctocrabBuilder<Svc, Config, Auth, LayerReady>
506where
507    Svc: Service<Request<OctoBody>, Response = Response<B>> + Send + 'static,
508    Svc::Future: Send + 'static,
509    Svc::Error: Into<BoxError>,
510    B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
511    B::Error: Into<BoxError>,
512{
513    /// Add a [`Layer`] to the current [`Service`] stack.
514    pub fn with_layer<L: Layer<Svc>>(
515        self,
516        layer: &L,
517    ) -> OctocrabBuilder<L::Service, Config, Auth, LayerReady> {
518        let Self {
519            service: stack,
520            auth,
521            config,
522            executor,
523            ..
524        } = self;
525        OctocrabBuilder {
526            service: layer.layer(stack),
527            auth,
528            config,
529            executor,
530            _layer_ready: PhantomData,
531        }
532    }
533}
534
535impl Default for OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
536    fn default() -> OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
537        OctocrabBuilder::new_empty().with_config(DefaultOctocrabBuilderConfig::default())
538    }
539}
540
541impl<Svc, Auth, LayerState> OctocrabBuilder<Svc, NoConfig, Auth, LayerState> {
542    fn with_config<Config>(self, config: Config) -> OctocrabBuilder<Svc, Config, Auth, LayerState> {
543        OctocrabBuilder {
544            service: self.service,
545            auth: self.auth,
546            executor: self.executor,
547            config,
548            _layer_ready: PhantomData,
549        }
550    }
551}
552
553impl<Svc, B, LayerState> OctocrabBuilder<Svc, NoConfig, AuthState, LayerState>
554where
555    Svc: Service<Request<OctoBody>, Response = Response<B>> + Send + 'static,
556    Svc::Future: Send + 'static,
557    Svc::Error: Into<BoxError>,
558    B: http_body::Body<Data = bytes::Bytes> + Send + Sync + 'static,
559    B::Error: Into<BoxError>,
560{
561    /// Build a [`Client`](OctocrabService) instance with the current [`Service`] stack.
562    pub fn build(self) -> Result<Octocrab, Infallible> {
563        // Transform response body to `BoxBody<Bytes, crate::Error>` and use type erased error to avoid type parameters.
564        let service = MapResponseBodyLayer::new(|b: B| {
565            b.map_err(|e| ServiceSnafu.into_error(e.into())).boxed()
566        })
567        .layer(self.service)
568        .map_err(|e| e.into());
569
570        if let Some(executor) = self.executor {
571            return Ok(Octocrab::new_with_executor(service, self.auth, executor));
572        }
573
574        Ok(Octocrab::new(service, self.auth))
575    }
576}
577
578impl<Svc, Config, LayerState> OctocrabBuilder<Svc, Config, NoAuth, LayerState> {
579    pub fn with_auth<Auth>(self, auth: Auth) -> OctocrabBuilder<Svc, Config, Auth, LayerState> {
580        OctocrabBuilder {
581            service: self.service,
582            auth,
583            config: self.config,
584            executor: self.executor,
585            _layer_ready: PhantomData,
586        }
587    }
588}
589
590impl OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
591    /// Set the retry configuration
592    #[cfg(feature = "retry")]
593    #[cfg_attr(docsrs, doc(cfg(feature = "retry")))]
594    pub fn add_retry_config(mut self, retry_config: RetryConfig) -> Self {
595        self.config.retry_config = retry_config;
596        self
597    }
598
599    /// Set the connect timeout.
600    #[cfg(feature = "timeout")]
601    #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
602    pub fn set_connect_timeout(mut self, timeout: Option<Duration>) -> Self {
603        self.config.connect_timeout = timeout;
604        self
605    }
606
607    /// Set the read timeout.
608    #[cfg(feature = "timeout")]
609    #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
610    pub fn set_read_timeout(mut self, timeout: Option<Duration>) -> Self {
611        self.config.read_timeout = timeout;
612        self
613    }
614
615    /// Set the write timeout.
616    #[cfg(feature = "timeout")]
617    #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
618    pub fn set_write_timeout(mut self, timeout: Option<Duration>) -> Self {
619        self.config.write_timeout = timeout;
620        self
621    }
622
623    /// Enable a GitHub preview.
624    pub fn add_preview(mut self, preview: &'static str) -> Self {
625        self.config.previews.push(preview);
626        self
627    }
628
629    /// Add an additional header to include with every request.
630    pub fn add_header(mut self, key: HeaderName, value: String) -> Self {
631        self.config.extra_headers.push((key, value));
632        self
633    }
634
635    /// Add a personal token to use for authentication.
636    pub fn personal_token<S: Into<SecretString>>(mut self, token: S) -> Self {
637        self.config.auth = Auth::PersonalToken(token.into());
638        self
639    }
640
641    /// Authenticate as a Github App.
642    /// `key`: RSA private key in DER or PEM formats.
643    pub fn app(mut self, app_id: AppId, key: jsonwebtoken::EncodingKey) -> Self {
644        self.config.auth = Auth::App(AppAuth { app_id, key });
645        self
646    }
647
648    /// Authenticate as a Basic Auth
649    /// username and password
650    pub fn basic_auth(mut self, username: String, password: String) -> Self {
651        self.config.auth = Auth::Basic { username, password };
652        self
653    }
654
655    /// Authenticate with an OAuth token.
656    pub fn oauth(mut self, oauth: auth::OAuth) -> Self {
657        self.config.auth = Auth::OAuth(oauth);
658        self
659    }
660
661    /// Authenticate with a user access token.
662    pub fn user_access_token<S: Into<SecretString>>(mut self, token: S) -> Self {
663        self.config.auth = Auth::UserAccessToken(token.into());
664        self
665    }
666
667    /// Set the base url for `Octocrab`.
668    pub fn base_uri(mut self, base_uri: impl TryInto<Uri>) -> Result<Self> {
669        self.config.base_uri = Some(
670            base_uri
671                .try_into()
672                .map_err(|_| UriParseError {})
673                .context(UriParseSnafu)?,
674        );
675        Ok(self)
676    }
677
678    /// Set the base upload url for `Octocrab`.
679    pub fn upload_uri(mut self, upload_uri: impl TryInto<Uri>) -> Result<Self> {
680        self.config.upload_uri = Some(
681            upload_uri
682                .try_into()
683                .map_err(|_| UriParseError {})
684                .context(UriParseSnafu)?,
685        );
686        Ok(self)
687    }
688
689    pub fn cache<C>(mut self, cache: C) -> Self
690    where
691        C: CacheStorage + 'static,
692    {
693        self.config.cache_storage = Some(Arc::new(cache));
694        self
695    }
696
697    #[cfg(feature = "retry")]
698    #[cfg_attr(docsrs, doc(cfg(feature = "retry")))]
699    pub fn set_connector_retry_service<S>(
700        &self,
701        connector: hyper_util::client::legacy::Client<S, OctoBody>,
702    ) -> Retry<RetryConfig, hyper_util::client::legacy::Client<S, OctoBody>> {
703        let retry_layer = RetryLayer::new(self.config.retry_config.clone());
704
705        retry_layer.layer(connector)
706    }
707
708    #[cfg(feature = "timeout")]
709    #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
710    pub fn set_connect_timeout_service<T>(&self, connector: T) -> TimeoutConnector<T>
711    where
712        T: Service<Uri> + Send,
713        T::Response: hyper::rt::Read + hyper::rt::Write + Send + Unpin,
714        T::Future: Send + 'static,
715        T::Error: Into<BoxError>,
716    {
717        let mut connector = TimeoutConnector::new(connector);
718        // Set the timeouts for the client
719        connector.set_connect_timeout(self.config.connect_timeout);
720        connector.set_read_timeout(self.config.read_timeout);
721        connector.set_write_timeout(self.config.write_timeout);
722        connector
723    }
724
725    /// Build a [`Client`](hyper_util::client::legacy::Client) instance with the current [`Service`] stack.
726    #[cfg(feature = "default-client")]
727    #[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
728    pub fn build(self) -> Result<Octocrab> {
729        let client: hyper_util::client::legacy::Client<_, OctoBody> = {
730            #[cfg(all(not(feature = "opentls"), not(feature = "rustls")))]
731            let mut connector = hyper::client::conn::http1::HttpConnector::new();
732
733            #[cfg(all(feature = "rustls", not(feature = "opentls")))]
734            let connector = {
735                let builder = HttpsConnectorBuilder::new();
736                #[cfg(feature = "rustls-webpki-tokio")]
737                let builder = builder.with_webpki_roots();
738                #[cfg(not(feature = "rustls-webpki-tokio"))]
739                let builder = builder
740                    .with_native_roots()
741                    .map_err(Into::into)
742                    .context(error::OtherSnafu)?; // enabled the `rustls-native-certs` feature in hyper-rustls
743
744                builder
745                    .https_or_http() //  Disable .https_only() during tests until: https://github.com/LukeMathWalker/wiremock-rs/issues/58 is resolved. Alternatively we can use conditional compilation to only enable this feature in tests, but it becomes rather ugly with integration tests.
746                    .enable_http1()
747                    .build()
748            };
749
750            #[cfg(all(feature = "opentls", not(feature = "rustls")))]
751            let connector = HttpsConnector::new();
752
753            #[cfg(feature = "timeout")]
754            let connector = self.set_connect_timeout_service(connector);
755
756            hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
757                .build(connector)
758        };
759
760        #[cfg(feature = "retry")]
761        let client = self.set_connector_retry_service(client);
762
763        #[cfg(feature = "tracing")]
764        let client = TraceLayer::new_for_http()
765            .make_span_with(|req: &Request<OctoBody>| {
766                tracing::debug_span!(
767                    "HTTP",
768                     http.method = %req.method(),
769                     http.url = %req.uri(),
770                     http.status_code = tracing::field::Empty,
771                     otel.name = req.extensions().get::<&'static str>().unwrap_or(&"HTTP"),
772                     otel.kind = "client",
773                     otel.status_code = tracing::field::Empty,
774                )
775            })
776            .on_request(|_req: &Request<OctoBody>, _span: &Span| {
777                tracing::debug!("requesting");
778            })
779            .on_response(
780                |res: &Response<hyper::body::Incoming>, _latency: Duration, span: &Span| {
781                    let status = res.status();
782                    span.record("http.status_code", status.as_u16());
783                    if status.is_client_error() || status.is_server_error() {
784                        span.record("otel.status_code", "ERROR");
785                    }
786                },
787            )
788            // Explicitly disable `on_body_chunk`. The default does nothing.
789            .on_body_chunk(())
790            .on_eos(|_: Option<&HeaderMap>, _duration: Duration, _span: &Span| {
791                tracing::debug!("stream closed");
792            })
793            .on_failure(
794                |ec: ServerErrorsFailureClass, _latency: Duration, span: &Span| {
795                    // Called when
796                    // - Calling the inner service errored
797                    // - Polling `Body` errored
798                    // - the response was classified as failure (5xx)
799                    // - End of stream was classified as failure
800                    span.record("otel.status_code", "ERROR");
801                    match ec {
802                        ServerErrorsFailureClass::StatusCode(status) => {
803                            span.record("http.status_code", status.as_u16());
804                            tracing::error!("failed with status {}", status)
805                        }
806                        ServerErrorsFailureClass::Error(err) => {
807                            tracing::error!("failed with error {}", err)
808                        }
809                    }
810                },
811            )
812            .layer(client);
813
814        #[cfg(feature = "follow-redirect")]
815        let client = tower_http::follow_redirect::FollowRedirectLayer::new().layer(client);
816
817        let mut hmap: Vec<(HeaderName, HeaderValue)> = vec![];
818
819        // Add the user agent header required by GitHub
820        hmap.push((USER_AGENT, HeaderValue::from_str("octocrab").unwrap()));
821
822        for preview in &self.config.previews {
823            hmap.push((
824                http::header::ACCEPT,
825                HeaderValue::from_str(crate::format_preview(preview).as_str()).unwrap(),
826            ));
827        }
828
829        let (auth_header, auth_state): (Option<HeaderValue>, _) = match self.config.auth {
830            Auth::None => (None, AuthState::None),
831            Auth::Basic { username, password } => {
832                (None, AuthState::BasicAuth { username, password })
833            }
834            Auth::PersonalToken(token) => (
835                Some(format!("Bearer {}", token.expose_secret()).parse().unwrap()),
836                AuthState::None,
837            ),
838            Auth::UserAccessToken(token) => (
839                Some(format!("Bearer {}", token.expose_secret()).parse().unwrap()),
840                AuthState::None,
841            ),
842            Auth::App(app_auth) => (None, AuthState::App(app_auth)),
843            Auth::OAuth(device) => (
844                Some(
845                    format!(
846                        "{} {}",
847                        device.token_type,
848                        &device.access_token.expose_secret()
849                    )
850                    .parse()
851                    .unwrap(),
852                ),
853                AuthState::None,
854            ),
855        };
856
857        for (key, value) in self.config.extra_headers.iter() {
858            hmap.push((
859                key.clone(),
860                HeaderValue::from_str(value.as_str())
861                    .map_err(http::Error::from)
862                    .context(HttpSnafu)?,
863            ));
864        }
865
866        let client = ExtraHeadersLayer::new(Arc::new(hmap)).layer(client);
867
868        let client = MapResponseBodyLayer::new(|body| {
869            BodyExt::map_err(body, |e| HyperSnafu.into_error(e)).boxed()
870        })
871        .layer(client);
872
873        let base_uri = self
874            .config
875            .base_uri
876            .clone()
877            .unwrap_or_else(|| Uri::from_str(GITHUB_BASE_URI).unwrap());
878
879        let upload_uri = self
880            .config
881            .upload_uri
882            .clone()
883            .unwrap_or_else(|| Uri::from_str(GITHUB_BASE_UPLOAD_URI).unwrap());
884
885        let client = BaseUriLayer::new(base_uri.clone()).layer(client);
886
887        let client = AuthHeaderLayer::new(auth_header, base_uri, upload_uri).layer(client);
888
889        let client = HttpCacheLayer::new(self.config.cache_storage.clone()).layer(client);
890
891        if let Some(executor) = self.executor {
892            return Ok(Octocrab::new_with_executor(client, auth_state, executor));
893        }
894
895        Ok(Octocrab::new(client, auth_state))
896    }
897}
898
899pub struct DefaultOctocrabBuilderConfig {
900    auth: Auth,
901    previews: Vec<&'static str>,
902    extra_headers: Vec<(HeaderName, String)>,
903    #[cfg(feature = "timeout")]
904    connect_timeout: Option<Duration>,
905    #[cfg(feature = "timeout")]
906    read_timeout: Option<Duration>,
907    #[cfg(feature = "timeout")]
908    write_timeout: Option<Duration>,
909    base_uri: Option<Uri>,
910    upload_uri: Option<Uri>,
911    #[cfg(feature = "retry")]
912    retry_config: RetryConfig,
913    cache_storage: Option<Arc<dyn CacheStorage>>,
914}
915
916impl Default for DefaultOctocrabBuilderConfig {
917    fn default() -> Self {
918        Self {
919            auth: Auth::None,
920            previews: Vec::new(),
921            extra_headers: Vec::new(),
922            #[cfg(feature = "timeout")]
923            connect_timeout: None,
924            #[cfg(feature = "timeout")]
925            read_timeout: None,
926            #[cfg(feature = "timeout")]
927            write_timeout: None,
928            base_uri: None,
929            upload_uri: None,
930            #[cfg(feature = "retry")]
931            retry_config: RetryConfig::Simple(3),
932            cache_storage: None,
933        }
934    }
935}
936
937impl DefaultOctocrabBuilderConfig {
938    pub fn new() -> Self {
939        Self::default()
940    }
941}
942
943#[derive(Debug, Clone)]
944struct CachedTokenInner {
945    expiration: Option<DateTime<Utc>>,
946    secret: SecretString,
947}
948
949impl CachedTokenInner {
950    fn new(secret: SecretString, expiration: Option<DateTime<Utc>>) -> Self {
951        Self { secret, expiration }
952    }
953
954    fn expose_secret(&self) -> &str {
955        self.secret.expose_secret()
956    }
957}
958
959/// A cached API access token (which may be None)
960pub struct CachedToken(RwLock<Option<CachedTokenInner>>);
961
962impl CachedToken {
963    fn clear(&self) {
964        *self.0.write().unwrap() = None;
965    }
966
967    /// Returns a valid token if it exists and is not expired or if there is no expiration date.
968    fn valid_token_with_buffer(&self, buffer: chrono::Duration) -> Option<SecretString> {
969        let inner = self.0.read().unwrap();
970
971        if let Some(token) = inner.as_ref() {
972            if let Some(exp) = token.expiration {
973                if exp - Utc::now() > buffer {
974                    return Some(token.secret.clone());
975                }
976            } else {
977                return Some(token.secret.clone());
978            }
979        }
980
981        None
982    }
983
984    fn valid_token(&self) -> Option<SecretString> {
985        self.valid_token_with_buffer(chrono::Duration::seconds(30))
986    }
987
988    fn set<S: Into<SecretString>>(&self, token: S, expiration: Option<DateTime<Utc>>) {
989        *self.0.write().unwrap() = Some(CachedTokenInner::new(token.into(), expiration));
990    }
991}
992
993impl fmt::Debug for CachedToken {
994    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
995        self.0.read().unwrap().fmt(f)
996    }
997}
998
999impl fmt::Display for CachedToken {
1000    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1001        let option = self.0.read().unwrap();
1002        option
1003            .as_ref()
1004            .map(|s| s.expose_secret().fmt(f))
1005            .unwrap_or_else(|| write!(f, "<none>"))
1006    }
1007}
1008
1009impl Clone for CachedToken {
1010    fn clone(&self) -> CachedToken {
1011        CachedToken(RwLock::new(self.0.read().unwrap().clone()))
1012    }
1013}
1014
1015impl Default for CachedToken {
1016    fn default() -> CachedToken {
1017        CachedToken(RwLock::new(None))
1018    }
1019}
1020
1021/// State used for authenticate to Github
1022#[derive(Debug, Clone)]
1023pub enum AuthState {
1024    /// No state, although Auth::PersonalToken may have caused
1025    /// an Authorization HTTP header to be set to provide authentication.
1026    None,
1027    /// Basic Auth HTTP. (username:password)
1028    BasicAuth {
1029        /// The username
1030        username: String,
1031        /// The password
1032        password: String,
1033    },
1034    /// Github App authentication with the given app data
1035    App(AppAuth),
1036    /// Authentication via a Github App repo-specific installation
1037    Installation {
1038        /// The app authentication data (app ID and private key)
1039        app: AppAuth,
1040        /// The installation ID
1041        installation: InstallationId,
1042        /// The cached access token, if any
1043        token: CachedToken,
1044    },
1045    /// Access token based authentication.
1046    AccessToken {
1047        /// The access token
1048        token: SecretString,
1049    },
1050}
1051
1052pub type OctocrabService = Buffer<
1053    http::Request<OctoBody>,
1054    <BoxService<http::Request<OctoBody>, http::Response<BoxBody<Bytes, Error>>, BoxError> as tower::Service<http::Request<OctoBody>>>::Future
1055>;
1056
1057/// The GitHub API client.
1058#[derive(Clone)]
1059pub struct Octocrab {
1060    client: OctocrabService,
1061    auth_state: AuthState,
1062}
1063
1064impl fmt::Debug for Octocrab {
1065    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1066        f.debug_struct("Octocrab")
1067            .field("auth_state", &self.auth_state)
1068            .finish()
1069    }
1070}
1071
1072/// Defaults for Octocrab:
1073/// - `base_uri`: `https://api.github.com`
1074/// - `auth`: `None`
1075/// - `client`: http client with the `octocrab` user agent.
1076#[cfg(feature = "default-client")]
1077#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
1078impl Default for Octocrab {
1079    fn default() -> Self {
1080        OctocrabBuilder::default().build().unwrap()
1081    }
1082}
1083
1084/// # Constructors
1085impl Octocrab {
1086    /// Returns a new `OctocrabBuilder`.
1087    pub fn builder() -> OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady>
1088    {
1089        OctocrabBuilder::new_empty().with_config(DefaultOctocrabBuilderConfig::default())
1090    }
1091
1092    /// Creates a new `Octocrab`.
1093    fn new<S>(service: S, auth_state: AuthState) -> Self
1094    where
1095        S: Service<Request<OctoBody>, Response = Response<BoxBody<Bytes, crate::Error>>>
1096            + Send
1097            + 'static,
1098        S::Future: Send + 'static,
1099        S::Error: Into<BoxError>,
1100    {
1101        let service = Buffer::new(BoxService::new(service.map_err(Into::into)), 1024);
1102
1103        Self {
1104            client: service,
1105            auth_state,
1106        }
1107    }
1108
1109    /// Creates a new `Octocrab` with a custom executor
1110    fn new_with_executor<S>(service: S, auth_state: AuthState, executor: Executor) -> Self
1111    where
1112        S: Service<Request<OctoBody>, Response = Response<BoxBody<Bytes, crate::Error>>>
1113            + Send
1114            + 'static,
1115        S::Future: Send + 'static,
1116        S::Error: Into<BoxError>,
1117    {
1118        // Use Buffer pair to return the background worker
1119        let (service, worker) = Buffer::pair(BoxService::new(service.map_err(Into::into)), 1024);
1120
1121        // Execute the background worker with the custom executor
1122        executor(Box::pin(worker));
1123
1124        Self {
1125            client: service,
1126            auth_state,
1127        }
1128    }
1129
1130    /// Returns a new `Octocrab` based on the current builder but
1131    /// authorizing via a specific installation ID.
1132    /// Typically you will first construct an `Octocrab` using
1133    /// `OctocrabBuilder::app` to authenticate as your Github App,
1134    /// then obtain an installation ID, and then pass that here to
1135    /// obtain a new `Octocrab` with which you can make API calls
1136    /// with the permissions of that installation.
1137    pub fn installation(&self, id: InstallationId) -> Result<Octocrab> {
1138        let app_auth = if let AuthState::App(ref app_auth) = self.auth_state {
1139            app_auth.clone()
1140        } else {
1141            return Err(Error::Installation {
1142                backtrace: Backtrace::capture(),
1143            });
1144        };
1145        Ok(Octocrab {
1146            client: self.client.clone(),
1147            auth_state: AuthState::Installation {
1148                app: app_auth,
1149                installation: id,
1150                token: CachedToken::default(),
1151            },
1152        })
1153    }
1154
1155    /// Similar to `installation`, but also eagerly caches the installation
1156    /// token and returns the token. The returned token can be used to make
1157    /// https git requests to e.g. clone repositories that the installation
1158    /// has access to.
1159    ///
1160    /// See also <https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#http-based-git-access-by-an-installation>
1161    pub async fn installation_and_token(
1162        &self,
1163        id: InstallationId,
1164    ) -> Result<(Octocrab, SecretString)> {
1165        let crab = self.installation(id)?;
1166        let token = crab.request_installation_auth_token().await?;
1167        Ok((crab, token))
1168    }
1169
1170    /// Acquire a GitHub App installation access token that does not expire for
1171    /// at least 30 seconds. A cached token will be used if its expiration is
1172    /// far enough in the future. Otherwise, a new token will be acquired and
1173    /// cached.
1174    pub async fn installation_token(&self) -> Result<SecretString> {
1175        self.installation_token_with_buffer(chrono::Duration::seconds(30))
1176            .await
1177    }
1178
1179    /// Acquire a GitHub App installation access token that does not expire for
1180    /// at least the duration specified by [`buffer`]. A cached token will be
1181    /// used if its expiration is far enough in the future. Otherwise, a new
1182    /// token will be acquired and cached.
1183    pub async fn installation_token_with_buffer(
1184        &self,
1185        buffer: chrono::Duration,
1186    ) -> Result<SecretString> {
1187        let token = if let AuthState::Installation { ref token, .. } = self.auth_state {
1188            token
1189        } else {
1190            return Err(Error::InstallationTokenInvalidAuth {
1191                backtrace: Backtrace::capture(),
1192            });
1193        };
1194
1195        let token = match token.valid_token_with_buffer(buffer) {
1196            Some(token) => token,
1197            None => self.request_installation_auth_token().await?,
1198        };
1199
1200        Ok(token)
1201    }
1202
1203    /// Returns a new `Octocrab` based on the current builder but
1204    /// authorizing via an access token.
1205    ///
1206    /// See also <https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app>
1207    pub fn user_access_token<S: Into<SecretString>>(&self, token: S) -> Result<Self> {
1208        Ok(Octocrab {
1209            client: self.client.clone(),
1210            auth_state: AuthState::AccessToken {
1211                token: token.into(),
1212            },
1213        })
1214    }
1215}
1216
1217/// # GitHub API Methods
1218impl Octocrab {
1219    /// Creates a new [`actions::ActionsHandler`] for accessing information from
1220    /// GitHub Actions.
1221    pub fn actions(&self) -> actions::ActionsHandler<'_> {
1222        actions::ActionsHandler::new(self)
1223    }
1224
1225    /// Creates a [`current::CurrentAuthHandler`] that allows you to access
1226    /// information about the current authenticated user.
1227    pub fn current(&self) -> current::CurrentAuthHandler<'_> {
1228        current::CurrentAuthHandler::new(self)
1229    }
1230
1231    /// Creates a [`activity::ActivityHandler`] for the current authenticated user.
1232    pub fn activity(&self) -> activity::ActivityHandler<'_> {
1233        activity::ActivityHandler::new(self)
1234    }
1235
1236    /// Creates a new [`apps::AppsRequestHandler`] for the currently authenticated app.
1237    pub fn apps(&self) -> apps::AppsRequestHandler<'_> {
1238        apps::AppsRequestHandler::new(self)
1239    }
1240
1241    /// Creates a [`gitignore::GitignoreHandler`] for accessing information
1242    /// about `gitignore`.
1243    pub fn gitignore(&self) -> gitignore::GitignoreHandler<'_> {
1244        gitignore::GitignoreHandler::new(self)
1245    }
1246
1247    /// Creates a [`issues::IssueHandler`] for the repo specified at `owner/repo`,
1248    /// that allows you to access GitHub's issues API.
1249    pub fn issues(
1250        &self,
1251        owner: impl Into<String>,
1252        repo: impl Into<String>,
1253    ) -> issues::IssueHandler<'_> {
1254        issues::IssueHandler::new(self, RepoRef::ByOwnerAndName(owner.into(), repo.into()))
1255    }
1256
1257    /// Creates a [`issues::IssueHandler`] for the repo specified at repository ID,
1258    /// that allows you to access GitHub's issues API.
1259    pub fn issues_by_id(&self, id: impl Into<RepositoryId>) -> issues::IssueHandler<'_> {
1260        issues::IssueHandler::new(self, RepoRef::ById(id.into()))
1261    }
1262
1263    /// Creates a [`code_scannings::CodeScanningHandler`] for the repo specified at `owner/repo`,
1264    /// that allows you to access GitHub's Code scanning API.
1265    pub fn code_scannings(
1266        &self,
1267        owner: impl Into<String>,
1268        repo: impl Into<String>,
1269    ) -> code_scannings::CodeScanningHandler<'_> {
1270        code_scannings::CodeScanningHandler::new(self, owner.into(), Option::from(repo.into()))
1271    }
1272
1273    /// Creates a [`code_scannings::CodeScanningHandler`] for the org specified at `owner`,
1274    /// that allows you to access GitHub's Code scanning API.
1275    pub fn code_scannings_organisation(
1276        &self,
1277        owner: impl Into<String>,
1278    ) -> code_scannings::CodeScanningHandler<'_> {
1279        code_scannings::CodeScanningHandler::new(self, owner.into(), None)
1280    }
1281
1282    /// Creates a [`commits::CommitHandler`] for the repo specified at `owner/repo`,
1283    pub fn commits(
1284        &self,
1285        owner: impl Into<String>,
1286        repo: impl Into<String>,
1287    ) -> commits::CommitHandler<'_> {
1288        commits::CommitHandler::new(self, owner.into(), repo.into())
1289    }
1290
1291    /// Creates a [`licenses::LicenseHandler`].
1292    pub fn licenses(&self) -> licenses::LicenseHandler<'_> {
1293        licenses::LicenseHandler::new(self)
1294    }
1295
1296    /// Creates a [`markdown::MarkdownHandler`].
1297    pub fn markdown(&self) -> markdown::MarkdownHandler<'_> {
1298        markdown::MarkdownHandler::new(self)
1299    }
1300
1301    /// Creates an [`orgs::OrgHandler`] for the specified organization,
1302    /// that allows you to access GitHub's organization API.
1303    pub fn orgs(&self, owner: impl Into<String>) -> orgs::OrgHandler<'_> {
1304        orgs::OrgHandler::new(self, owner.into())
1305    }
1306
1307    /// Creates a [`pulls::PullRequestHandler`] for the repo specified at
1308    /// `owner/repo`, that allows you to access GitHub's pull request API.
1309    pub fn pulls(
1310        &self,
1311        owner: impl Into<String>,
1312        repo: impl Into<String>,
1313    ) -> pulls::PullRequestHandler<'_> {
1314        pulls::PullRequestHandler::new(self, owner.into(), repo.into())
1315    }
1316
1317    /// Creates a [`repos::RepoHandler`] for the repo specified at `owner/repo`,
1318    /// that allows you to access GitHub's repository API.
1319    pub fn repos(
1320        &self,
1321        owner: impl Into<String>,
1322        repo: impl Into<String>,
1323    ) -> repos::RepoHandler<'_> {
1324        repos::RepoHandler::new(self, RepoRef::ByOwnerAndName(owner.into(), repo.into()))
1325    }
1326
1327    /// Creates a [`repos::RepoHandler`] for the repo specified at repository ID,
1328    /// that allows you to access GitHub's repository API.
1329    pub fn repos_by_id(&self, id: impl Into<RepositoryId>) -> repos::RepoHandler<'_> {
1330        repos::RepoHandler::new(self, RepoRef::ById(id.into()))
1331    }
1332
1333    /// Creates a [`projects::ProjectHandler`] that allows you to access GitHub's
1334    /// projects API (classic).
1335    pub fn projects(&self) -> projects::ProjectHandler<'_> {
1336        projects::ProjectHandler::new(self)
1337    }
1338
1339    /// Creates a [`search::SearchHandler`] that allows you to construct general queries
1340    /// to GitHub's API.
1341    pub fn search(&self) -> search::SearchHandler<'_> {
1342        search::SearchHandler::new(self)
1343    }
1344
1345    /// Creates a [`teams::TeamHandler`] for the specified organization that allows
1346    /// you to access GitHub's teams API.
1347    pub fn teams(&self, owner: impl Into<String>) -> teams::TeamHandler<'_> {
1348        teams::TeamHandler::new(self, owner.into())
1349    }
1350
1351    /// Creates a [`users::UserHandler`] for the specified user using the user name
1352    pub fn users(&self, user: impl Into<String>) -> users::UserHandler<'_> {
1353        users::UserHandler::new(self, UserRef::ByString(user.into()))
1354    }
1355
1356    /// Creates a [`users::UserHandler`] for the specified user using the user ID
1357    pub fn users_by_id(&self, user: impl Into<UserId>) -> users::UserHandler<'_> {
1358        users::UserHandler::new(self, UserRef::ById(user.into()))
1359    }
1360
1361    /// Creates a [`workflows::WorkflowsHandler`] for the specified repository that allows
1362    /// you to access GitHub's workflows API.
1363    pub fn workflows(
1364        &self,
1365        owner: impl Into<String>,
1366        repo: impl Into<String>,
1367    ) -> workflows::WorkflowsHandler<'_> {
1368        workflows::WorkflowsHandler::new(self, owner.into(), repo.into())
1369    }
1370
1371    /// Creates an [`events::EventsBuilder`] that allows you to access
1372    /// GitHub's events API.
1373    pub fn events(&self) -> events::EventsBuilder<'_> {
1374        events::EventsBuilder::new(self)
1375    }
1376
1377    /// Creates a [`gists::GistsHandler`] that allows you to access
1378    /// GitHub's Gists API.
1379    pub fn gists(&self) -> gists::GistsHandler<'_> {
1380        gists::GistsHandler::new(self)
1381    }
1382
1383    /// Creates a [`checks::ChecksHandler`] that allows to access the Checks API.
1384    pub fn checks(
1385        &self,
1386        owner: impl Into<String>,
1387        repo: impl Into<String>,
1388    ) -> checks::ChecksHandler<'_> {
1389        checks::ChecksHandler::new(self, owner.into(), repo.into())
1390    }
1391
1392    /// Creates a [`ratelimit::RateLimitHandler`] that returns the API rate limit.
1393    pub fn ratelimit(&self) -> ratelimit::RateLimitHandler<'_> {
1394        ratelimit::RateLimitHandler::new(self)
1395    }
1396
1397    /// Creates a [`hooks::HooksHandler`] that returns the API hooks
1398    pub fn hooks(&self, owner: impl Into<String>) -> hooks::HooksHandler<'_> {
1399        hooks::HooksHandler::new(self, owner.into())
1400    }
1401
1402    /// Creates a [`classroom::AssignmentsHandler`] providing the GitHub Classroom _Assignments_ API
1403    pub fn assignments(&self) -> classroom::AssignmentsHandler<'_> {
1404        classroom::AssignmentsHandler::new(self)
1405    }
1406
1407    /// Creates a [`classroom::ClassroomHandler`] providing the GitHub Classroom _Classrooms_ API
1408    pub fn classrooms(&self) -> classroom::ClassroomHandler<'_> {
1409        classroom::ClassroomHandler::new(self)
1410    }
1411
1412    /// Creates a [`codes_of_conduct::CodesOfConductHandler`] providing the GitHub Codes of Codes of Conduct API
1413    pub fn codes_of_conduct(&self) -> codes_of_conduct::CodesOfConductHandler<'_> {
1414        codes_of_conduct::CodesOfConductHandler::new(self)
1415    }
1416}
1417
1418/// # GraphQL API.
1419impl Octocrab {
1420    /// Sends a graphql query to GitHub, and deserialises the response
1421    /// from JSON.
1422    /// ```no_run
1423    ///# async fn run() -> octocrab::Result<()> {
1424    /// let response: octocrab::GraphqlResponse<serde_json::Value> = octocrab::instance()
1425    ///     .graphql(&serde_json::json!({ "query": "{ viewer { login }}" }))
1426    ///     .await?;
1427    ///# Ok(())
1428    ///# }
1429    /// ```
1430    pub async fn graphql<R: serde::de::DeserializeOwned>(
1431        &self,
1432        payload: &(impl serde::Serialize + ?Sized),
1433    ) -> crate::Result<R> {
1434        let response: GraphqlResponse<R> = self
1435            .post("/graphql", Some(&serde_json::json!(payload)))
1436            .await?;
1437
1438        match response {
1439            GraphqlResponse::Ok(res) => Ok(res.data),
1440            GraphqlResponse::Err(errors) => Err(error::Error::Graphql {
1441                source: errors.errors.into(),
1442                backtrace: Backtrace::capture(),
1443            }),
1444        }
1445    }
1446}
1447
1448/// GraphQL Response.
1449/// GraphQL can return a response with `data` or `errors`, or both in the case of a partial success.
1450#[derive(Serialize, Deserialize, Debug)]
1451#[serde(untagged)]
1452pub enum GraphqlResponse<T> {
1453    /// A response containing errors.
1454    Err(GraphqlErrorResponse<T>),
1455    /// A response representing a complete success with no errors.
1456    Ok(GraphqlOkResponse<T>),
1457}
1458
1459#[derive(Serialize, Deserialize, Debug)]
1460pub struct GraphqlOkResponse<T> {
1461    pub data: T,
1462}
1463
1464#[derive(Serialize, Deserialize, Debug)]
1465pub struct GraphqlErrorResponse<T> {
1466    /// GraphQL returns `data` even in the case of a partial success.
1467    pub data: Option<T>,
1468    /// A list of errors encountered during the request.
1469    pub errors: Vec<GraphqlError>,
1470}
1471
1472/// An individual GraphQL error.
1473/// Following the [GraphQL October 2021 Spec](https://spec.graphql.org/October2021/#sec-Errors).
1474#[derive(Serialize, Deserialize, Debug)]
1475pub struct GraphqlError {
1476    /// A description of the error intended for the developer as a guide to understand and correct the error.
1477    pub message: String,
1478    /// A particular point of reference in the GraphQL query where the error occurred.
1479    /// This may be `None` if the error cannot be associated with a particular point
1480    pub locations: Option<Vec<GraphqlErrorLocation>>,
1481    /// The path to the specific field that caused the error.
1482    /// This may be `None` if the error is not associated with a specific field
1483    pub path: Option<Vec<GraphqlPathSegment>>,
1484    /// Additional error metadata provided by the server.
1485    pub extensions: Option<serde_json::Value>,
1486}
1487
1488#[derive(Serialize, Deserialize, Debug)]
1489pub struct GraphqlErrorLocation {
1490    pub line: u32,
1491    pub column: u32,
1492}
1493
1494/// A path can consist of field names (Strings) and list indices (usize).
1495#[derive(Serialize, Deserialize, Debug)]
1496#[serde(untagged)]
1497pub enum GraphqlPathSegment {
1498    Path(String),
1499    Position(usize),
1500}
1501
1502/// # HTTP Methods
1503/// A collection of different of HTTP methods to use with Octocrab's
1504/// configuration (Authenication, etc.). All of the HTTP methods (`get`, `post`,
1505/// etc.) perform some amount of pre-processing such as making relative urls
1506/// absolute, and post processing such as mapping any potential GitHub errors
1507/// into `Err()` variants, and deserializing the response body.
1508///
1509/// This isn't always ideal when working with GitHub's API and as such there are
1510/// additional methods available prefixed with `_` (e.g.  `_get`, `_post`,
1511/// etc.) that perform no pre or post processing and directly return the
1512/// `http::Response` struct.
1513impl Octocrab {
1514    /// Send a `POST` request to `route` with an optional body, returning the body
1515    /// of the response.
1516    pub async fn post<P: Serialize + ?Sized, R: FromResponse>(
1517        &self,
1518        route: impl AsRef<str>,
1519        body: Option<&P>,
1520    ) -> Result<R> {
1521        let response = self
1522            ._post(self.parameterized_uri(route, None::<&()>)?, body)
1523            .await?;
1524        R::from_response(crate::map_github_error(response).await?).await
1525    }
1526
1527    /// Send a `POST` request with no additional pre/post-processing.
1528    pub async fn _post<P: Serialize + ?Sized>(
1529        &self,
1530        uri: impl TryInto<http::Uri>,
1531        body: Option<&P>,
1532    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1533        let uri = uri
1534            .try_into()
1535            .map_err(|_| UriParseError {})
1536            .context(UriParseSnafu)?;
1537        let request = Builder::new().method(Method::POST).uri(uri);
1538        let request = self.build_request(request, body)?;
1539        self.execute(request).await
1540    }
1541
1542    /// Send a `GET` request to `route` with optional query parameters, returning
1543    /// the body of the response.
1544    pub async fn get<R, A, P>(&self, route: A, parameters: Option<&P>) -> Result<R>
1545    where
1546        A: AsRef<str>,
1547        P: Serialize + ?Sized,
1548        R: FromResponse,
1549    {
1550        self.get_with_headers(route, parameters, None).await
1551    }
1552
1553    /// Send a `GET` request with no additional post-processing.
1554    pub async fn _get(
1555        &self,
1556        uri: impl TryInto<Uri>,
1557    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1558        self._get_with_headers(uri, None).await
1559    }
1560
1561    /// Convenience method to accept any &str, and attempt to convert it to a Uri.
1562    /// the method also attempts to serialize any parameters into a query string, and append it to the uri.
1563    pub(crate) fn parameterized_uri<A, P>(&self, uri: A, parameters: Option<&P>) -> Result<Uri>
1564    where
1565        A: AsRef<str>,
1566        P: Serialize + ?Sized,
1567    {
1568        let mut uri = uri.as_ref().to_string();
1569        if let Some(parameters) = parameters {
1570            if uri.contains('?') {
1571                uri = format!("{uri}&");
1572            } else {
1573                uri = format!("{uri}?");
1574            }
1575            uri = format!(
1576                "{}{}",
1577                uri,
1578                serde_urlencoded::to_string(parameters)
1579                    .context(SerdeUrlEncodedSnafu)?
1580                    .as_str()
1581            );
1582        }
1583        let uri = Uri::from_str(uri.as_str()).context(UriSnafu);
1584        uri
1585    }
1586
1587    pub async fn body_to_string(
1588        &self,
1589        res: http::Response<BoxBody<Bytes, crate::Error>>,
1590    ) -> Result<String> {
1591        let body_bytes = res.into_body().collect().await?.to_bytes();
1592        String::from_utf8(body_bytes.to_vec()).context(InvalidUtf8Snafu)
1593    }
1594
1595    /// Send a `GET` request to `route` with optional query parameters and headers, returning
1596    /// the body of the response.
1597    pub async fn get_with_headers<R, A, P>(
1598        &self,
1599        route: A,
1600        parameters: Option<&P>,
1601        headers: Option<http::header::HeaderMap>,
1602    ) -> Result<R>
1603    where
1604        A: AsRef<str>,
1605        P: Serialize + ?Sized,
1606        R: FromResponse,
1607    {
1608        let response = self
1609            ._get_with_headers(self.parameterized_uri(route, parameters)?, headers)
1610            .await?;
1611        R::from_response(crate::map_github_error(response).await?).await
1612    }
1613
1614    /// Send a `GET` request including option to set headers, with no additional post-processing.
1615    pub async fn _get_with_headers(
1616        &self,
1617        uri: impl TryInto<Uri>,
1618        headers: Option<http::header::HeaderMap>,
1619    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1620        let uri = uri
1621            .try_into()
1622            .map_err(|_| UriParseError {})
1623            .context(UriParseSnafu)?;
1624        let mut request = Builder::new().method(Method::GET).uri(uri);
1625        if let Some(headers) = headers {
1626            for (key, value) in headers.iter() {
1627                request = request.header(key, value);
1628            }
1629        }
1630        let request = self.build_request(request, None::<&()>)?;
1631        self.execute(request).await
1632    }
1633
1634    /// Send a `PATCH` request to `route` with optional query parameters,
1635    /// returning the body of the response.
1636    pub async fn patch<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1637    where
1638        A: AsRef<str>,
1639        B: Serialize + ?Sized,
1640        R: FromResponse,
1641    {
1642        let response = self
1643            ._patch(self.parameterized_uri(route, None::<&()>)?, body)
1644            .await?;
1645        R::from_response(crate::map_github_error(response).await?).await
1646    }
1647
1648    /// Send a `PATCH` request with no additional post-processing.
1649    pub async fn _patch<B: Serialize + ?Sized>(
1650        &self,
1651        uri: impl TryInto<Uri>,
1652        body: Option<&B>,
1653    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1654        let uri = uri
1655            .try_into()
1656            .map_err(|_| UriParseError {})
1657            .context(UriParseSnafu)?;
1658        let request = Builder::new().method(Method::PATCH).uri(uri);
1659        let request = self.build_request(request, body)?;
1660        self.execute(request).await
1661    }
1662
1663    /// Send a `PUT` request to `route` with optional query parameters,
1664    /// returning the body of the response.
1665    pub async fn put<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1666    where
1667        A: AsRef<str>,
1668        B: Serialize + ?Sized,
1669        R: FromResponse,
1670    {
1671        let response = self
1672            ._put(self.parameterized_uri(route, None::<&()>)?, body)
1673            .await?;
1674        R::from_response(crate::map_github_error(response).await?).await
1675    }
1676
1677    /// Send a `PUT` request with no additional post-processing.
1678    pub async fn _put<B: Serialize + ?Sized>(
1679        &self,
1680        uri: impl TryInto<Uri>,
1681        body: Option<&B>,
1682    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1683        let uri = uri
1684            .try_into()
1685            .map_err(|_| UriParseError {})
1686            .context(UriParseSnafu)?;
1687        let request = Builder::new().method(Method::PUT).uri(uri);
1688        let request = self.build_request(request, body)?;
1689        self.execute(request).await
1690    }
1691
1692    pub fn build_request<B: Serialize + ?Sized>(
1693        &self,
1694        mut builder: Builder,
1695        body: Option<&B>,
1696    ) -> Result<http::Request<OctoBody>> {
1697        // Since Octocrab doesn't require streamable bodies(aka, file upload) because it is serde::Serialize),
1698        // we can just use String body, since it is both http_body::Body(required by Hyper::Client), and Clone(required by BoxService).
1699
1700        // In case octocrab needs to support cases where body is strictly streamable, it should use something like reqwest::Body,
1701        // since it differentiates between retryable bodies, and streams(aka, it implements try_clone(), which is needed for middlewares like retry).
1702
1703        // Add headers specified in Cargo.toml
1704        // '[package.metadata.github-api].request-headers' section
1705        for kv in _SET_HEADERS_MAP {
1706            builder = builder.header(kv.0, kv.1);
1707        }
1708
1709        if let Some(body) = body {
1710            builder = builder.header(http::header::CONTENT_TYPE, "application/json");
1711            let serialized = serde_json::to_string(body).context(SerdeSnafu)?;
1712            let body: OctoBody = serialized.into();
1713            let request = builder.body(body).context(HttpSnafu)?;
1714            Ok(request)
1715        } else {
1716            Ok(builder
1717                .header(http::header::CONTENT_LENGTH, "0")
1718                .body(OctoBody::empty())
1719                .context(HttpSnafu)?)
1720        }
1721    }
1722
1723    /// Send a `DELETE` request to `route` with optional query body,
1724    /// returning the body of the response.
1725    pub async fn delete<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1726    where
1727        A: AsRef<str>,
1728        B: Serialize + ?Sized,
1729        R: FromResponse,
1730    {
1731        let response = self
1732            ._delete(self.parameterized_uri(route, None::<&()>)?, body)
1733            .await?;
1734        R::from_response(crate::map_github_error(response).await?).await
1735    }
1736
1737    /// Send a `DELETE` request with no additional post-processing.
1738    pub async fn _delete<B: Serialize + ?Sized>(
1739        &self,
1740        uri: impl TryInto<Uri>,
1741        body: Option<&B>,
1742    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1743        let uri = uri
1744            .try_into()
1745            .map_err(|_| UriParseError {})
1746            .context(UriParseSnafu)?;
1747        let request = self.build_request(Builder::new().method(Method::DELETE).uri(uri), body)?;
1748
1749        self.execute(request).await
1750    }
1751
1752    /// Requests a fresh installation auth token and caches it. Returns the token.
1753    async fn request_installation_auth_token(&self) -> Result<SecretString> {
1754        let (app, installation, token) = if let AuthState::Installation {
1755            ref app,
1756            installation,
1757            ref token,
1758        } = self.auth_state
1759        {
1760            (app, installation, token)
1761        } else {
1762            return Err(Error::Installation {
1763                backtrace: Backtrace::capture(),
1764            });
1765        };
1766        let mut request = Builder::new();
1767        let mut sensitive_value =
1768            HeaderValue::from_str(format!("Bearer {}", app.generate_bearer_token()?).as_str())
1769                .map_err(http::Error::from)
1770                .context(HttpSnafu)?;
1771
1772        let uri = http::Uri::builder()
1773            .path_and_query(format!("/app/installations/{installation}/access_tokens"))
1774            .build()
1775            .context(HttpSnafu)?;
1776
1777        sensitive_value.set_sensitive(true);
1778        request = request
1779            .header(http::header::AUTHORIZATION, sensitive_value)
1780            .method(http::Method::POST)
1781            .uri(uri);
1782        let response = self
1783            .send(request.body("{}".into()).context(HttpSnafu)?)
1784            .await?;
1785        let _status = response.status();
1786
1787        let token_object =
1788            InstallationToken::from_response(crate::map_github_error(response).await?).await?;
1789
1790        let expiration = token_object
1791            .expires_at
1792            .map(|time| {
1793                DateTime::<Utc>::from_str(&time).map_err(|e| error::Error::Other {
1794                    source: Box::new(e),
1795                    backtrace: snafu::Backtrace::capture(),
1796                })
1797            })
1798            .transpose()?;
1799
1800        #[cfg(feature = "tracing")]
1801        tracing::debug!("Token expires at: {:?}", expiration);
1802
1803        token.set(token_object.token.clone(), expiration);
1804
1805        Ok(SecretString::from(token_object.token))
1806    }
1807
1808    /// Send the given request to the underlying service
1809    pub async fn send(
1810        &self,
1811        request: Request<OctoBody>,
1812    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1813        let mut svc = self.client.clone();
1814        let response: Response<BoxBody<Bytes, crate::Error>> = svc
1815            .ready()
1816            .await
1817            .context(ServiceSnafu)?
1818            .call(request)
1819            .await
1820            .context(ServiceSnafu)?;
1821        Ok(response)
1822        //todo: attempt to downcast error to something more specific before returning. (Currently having trouble with this because I am not accustomed with snafu)
1823        // map_err(|err| {
1824        //     // Error decorating request
1825        //     err.downcast::<Error>()
1826        //         .map(|e| *e)
1827        //         // Error requesting
1828        //         .or_else(|err| err.downcast::<hyper::Error>().map(|err| Error::HyperError(*err)))
1829        //         // Error from another middleware
1830        //         .unwrap_or_else(|err| Error::Service(err))
1831        // })?;
1832    }
1833
1834    /// Execute the given `request` using octocrab's Client.
1835    pub async fn execute(
1836        &self,
1837        request: http::Request<impl Into<OctoBody>>,
1838    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1839        let (mut parts, body) = request.into_parts();
1840        let body: OctoBody = body.into();
1841        // Saved request that we can retry later if necessary
1842        let auth_header: Option<HeaderValue> = match self.auth_state {
1843            AuthState::None => None,
1844            AuthState::App(ref app) => Some(
1845                HeaderValue::from_str(format!("Bearer {}", app.generate_bearer_token()?).as_str())
1846                    .map_err(http::Error::from)
1847                    .context(HttpSnafu)?,
1848            ),
1849            AuthState::BasicAuth {
1850                ref username,
1851                ref password,
1852            } => {
1853                // Equivalent implementation of: https://github.com/seanmonstar/reqwest/blob/df2b3baadc1eade54b1c22415792b778442673a4/src/util.rs#L3-L23
1854                use base64::prelude::BASE64_STANDARD;
1855                use base64::write::EncoderWriter;
1856
1857                let mut buf = b"Basic ".to_vec();
1858                {
1859                    let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
1860                    write!(encoder, "{username}:{password}").expect("writing to a Vec never fails");
1861                }
1862                Some(HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"))
1863            }
1864            AuthState::Installation { ref token, .. } => {
1865                let token = if let Some(token) = token.valid_token() {
1866                    token
1867                } else {
1868                    self.request_installation_auth_token().await?
1869                };
1870
1871                Some(
1872                    HeaderValue::from_str(format!("Bearer {}", token.expose_secret()).as_str())
1873                        .map_err(http::Error::from)
1874                        .context(HttpSnafu)?,
1875                )
1876            }
1877            AuthState::AccessToken { ref token } => Some(
1878                HeaderValue::from_str(format!("Bearer {}", token.expose_secret()).as_str())
1879                    .map_err(http::Error::from)
1880                    .context(HttpSnafu)?,
1881            ),
1882        };
1883
1884        if let Some(mut auth_header) = auth_header {
1885            // Only set the auth_header if the authority (host) is api.github.com or empty (destined for
1886            // GitHub). Otherwise, leave it off as we could have been redirected
1887            // away from GitHub (via follow_location_to_data()), and we don't
1888            // want to give our credentials to third-party services.
1889            match parts.uri.authority() {
1890                None => {
1891                    auth_header.set_sensitive(true);
1892                    parts
1893                        .headers
1894                        .insert(http::header::AUTHORIZATION, auth_header);
1895                }
1896                Some(authority) if authority == "api.github.com" => {
1897                    auth_header.set_sensitive(true);
1898                    parts
1899                        .headers
1900                        .insert(http::header::AUTHORIZATION, auth_header);
1901                }
1902                Some(_) => {
1903                    // Don't insert auth header.
1904                }
1905            }
1906        }
1907
1908        let request = http::Request::from_parts(parts, body);
1909
1910        let response = self.send(request).await?;
1911
1912        let status = response.status();
1913        if StatusCode::UNAUTHORIZED == status {
1914            if let AuthState::Installation { ref token, .. } = self.auth_state {
1915                token.clear();
1916            }
1917        }
1918        Ok(response)
1919    }
1920
1921    pub async fn follow_location_to_data(
1922        &self,
1923        response: http::Response<BoxBody<Bytes, Error>>,
1924    ) -> crate::Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1925        if let Some(redirect) = response.headers().get(http::header::LOCATION) {
1926            let location = redirect.to_str().expect("Location URL not valid str");
1927
1928            self._get(location).await
1929        } else {
1930            Ok(response)
1931        }
1932    }
1933
1934    /// Download a file from the given URL with the given content type
1935    ///
1936    /// This is a convenience method that sets the `Accept` header to the given
1937    /// content type and downloads the file into a `Vec<u8>`.
1938    pub async fn download(
1939        &self,
1940        uri: impl TryInto<Uri>,
1941        content_type: impl TryInto<http::HeaderValue>,
1942    ) -> crate::Result<Vec<u8>> {
1943        let uri = uri
1944            .try_into()
1945            .map_err(|_| UriParseError {})
1946            .context(UriParseSnafu)?;
1947        let content_type = content_type
1948            .try_into()
1949            .map_err(|_| UriParseError {})
1950            .context(UriParseSnafu)?;
1951
1952        let mut request = Builder::new().method(Method::GET).uri(uri);
1953        request = request.header(http::header::ACCEPT, content_type);
1954
1955        let request = self.build_request(request, None::<&()>)?;
1956        let response = self.execute(request).await?;
1957
1958        let bytes = response.into_body().collect().await?.to_bytes();
1959        Ok(bytes.to_vec())
1960    }
1961
1962    /// Download a zip file from the given URL into a `Vec<u8>`.
1963    pub async fn download_zip(&self, uri: impl TryInto<Uri>) -> crate::Result<Vec<u8>> {
1964        self.download(uri, "application/zip").await
1965    }
1966}
1967
1968/// # Utility Methods
1969impl Octocrab {
1970    /// A convenience method to get a page of results (if present).
1971    pub async fn get_page<R: serde::de::DeserializeOwned>(
1972        &self,
1973        uri: &Option<Uri>,
1974    ) -> crate::Result<Option<Page<R>>> {
1975        match uri {
1976            Some(uri) => self.get(uri.to_string(), None::<&()>).await.map(Some),
1977            None => Ok(None),
1978        }
1979    }
1980
1981    /// A convenience method to get all the results starting at a given
1982    /// page.
1983    pub async fn all_pages<R: serde::de::DeserializeOwned>(
1984        &self,
1985        mut page: Page<R>,
1986    ) -> crate::Result<Vec<R>> {
1987        let mut ret = page.take_items();
1988        while let Some(mut next_page) = self.get_page(&page.next).await? {
1989            ret.append(&mut next_page.take_items());
1990            page = next_page;
1991        }
1992        Ok(ret)
1993    }
1994}
1995
1996#[cfg(test)]
1997mod tests {
1998    // tokio runtime seems to be needed for tower: https://users.rust-lang.org/t/no-reactor-running-when-calling-runtime-spawn/81256
1999    #[tokio::test]
2000    async fn parametrize_uri_valid() {
2001        //Previously, invalid characters were handled by url lib's parse function.
2002        //Todo: should we handle encoding of uri routes ourselves?
2003        let uri = crate::instance()
2004            .parameterized_uri("/help%20world", None::<&()>)
2005            .unwrap();
2006        assert_eq!(uri.path(), "/help%20world");
2007    }
2008
2009    #[tokio::test]
2010    async fn extra_headers() {
2011        use http::header::HeaderName;
2012        use wiremock::{matchers, Mock, MockServer, ResponseTemplate};
2013        let response = ResponseTemplate::new(304).append_header("etag", "\"abcd\"");
2014        let mock_server = MockServer::start().await;
2015        Mock::given(matchers::method("GET"))
2016            .and(matchers::path_regex(".*"))
2017            .and(matchers::header("x-test1", "hello"))
2018            .and(matchers::header("x-test2", "goodbye"))
2019            .respond_with(response)
2020            .expect(1)
2021            .mount(&mock_server)
2022            .await;
2023        crate::OctocrabBuilder::default()
2024            .base_uri(mock_server.uri())
2025            .unwrap()
2026            .add_header(HeaderName::from_static("x-test1"), "hello".to_string())
2027            .add_header(HeaderName::from_static("x-test2"), "goodbye".to_string())
2028            .build()
2029            .unwrap()
2030            .repos("XAMPPRocky", "octocrab")
2031            .events()
2032            .send()
2033            .await
2034            .unwrap();
2035    }
2036
2037    use super::*;
2038    use chrono::Duration;
2039
2040    #[test]
2041    fn clear_token() {
2042        let cache = CachedToken(RwLock::new(None));
2043        cache.set("secret".to_string(), None);
2044        cache.clear();
2045
2046        assert!(cache.valid_token().is_none(), "Token was not cleared.");
2047    }
2048
2049    #[test]
2050    fn no_token_when_expired() {
2051        let cache = CachedToken(RwLock::new(None));
2052        let expiration = Utc::now() + Duration::seconds(9);
2053        cache.set("secret".to_string(), Some(expiration));
2054
2055        assert!(
2056            cache
2057                .valid_token_with_buffer(Duration::seconds(10))
2058                .is_none(),
2059            "Token should be considered expired due to buffer."
2060        );
2061    }
2062
2063    #[test]
2064    fn get_valid_token_outside_buffer() {
2065        let cache = CachedToken(RwLock::new(None));
2066        let expiration = Utc::now() + Duration::seconds(12);
2067        cache.set("secret".to_string(), Some(expiration));
2068
2069        assert!(
2070            cache
2071                .valid_token_with_buffer(Duration::seconds(10))
2072                .is_some(),
2073            "Token should still be valid outside of buffer."
2074        );
2075    }
2076
2077    #[test]
2078    fn get_valid_token_without_expiration() {
2079        let cache = CachedToken(RwLock::new(None));
2080        cache.set("secret".to_string(), None);
2081
2082        assert!(
2083            cache
2084                .valid_token_with_buffer(Duration::seconds(10))
2085                .is_some(),
2086            "Token with no expiration should always be considered valid."
2087        );
2088    }
2089}