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
590#[cfg(all(feature = "rustls", not(feature = "opentls")))]
591fn default_rustls_crypto_provider() -> Arc<rustls::crypto::CryptoProvider> {
592    #[cfg(feature = "rustls-aws-lc-rs")]
593    {
594        Arc::new(rustls::crypto::aws_lc_rs::default_provider())
595    }
596    #[cfg(all(feature = "rustls-ring", not(feature = "rustls-aws-lc-rs")))]
597    {
598        Arc::new(rustls::crypto::ring::default_provider())
599    }
600    #[cfg(not(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring")))]
601    {
602        compile_error!(
603            "the `rustls` feature requires one of the `rustls-ring` or `rustls-aws-lc-rs` features to be enabled"
604        )
605    }
606}
607
608impl OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
609    /// Set the retry configuration
610    #[cfg(feature = "retry")]
611    #[cfg_attr(docsrs, doc(cfg(feature = "retry")))]
612    pub fn add_retry_config(mut self, retry_config: RetryConfig) -> Self {
613        self.config.retry_config = retry_config;
614        self
615    }
616
617    /// Set the connect timeout.
618    #[cfg(feature = "timeout")]
619    #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
620    pub fn set_connect_timeout(mut self, timeout: Option<Duration>) -> Self {
621        self.config.connect_timeout = timeout;
622        self
623    }
624
625    /// Set the read timeout.
626    #[cfg(feature = "timeout")]
627    #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
628    pub fn set_read_timeout(mut self, timeout: Option<Duration>) -> Self {
629        self.config.read_timeout = timeout;
630        self
631    }
632
633    /// Set the write timeout.
634    #[cfg(feature = "timeout")]
635    #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
636    pub fn set_write_timeout(mut self, timeout: Option<Duration>) -> Self {
637        self.config.write_timeout = timeout;
638        self
639    }
640
641    /// Enable a GitHub preview.
642    pub fn add_preview(mut self, preview: &'static str) -> Self {
643        self.config.previews.push(preview);
644        self
645    }
646
647    /// Add an additional header to include with every request.
648    pub fn add_header(mut self, key: HeaderName, value: String) -> Self {
649        self.config.extra_headers.push((key, value));
650        self
651    }
652
653    /// Add a personal token to use for authentication.
654    pub fn personal_token<S: Into<SecretString>>(mut self, token: S) -> Self {
655        self.config.auth = Auth::PersonalToken(token.into());
656        self
657    }
658
659    /// Authenticate as a Github App.
660    /// `key`: RSA private key in DER or PEM formats.
661    pub fn app(mut self, app_id: AppId, key: jsonwebtoken::EncodingKey) -> Self {
662        self.config.auth = Auth::App(AppAuth { app_id, key });
663        self
664    }
665
666    /// Authenticate as a Basic Auth
667    /// username and password
668    pub fn basic_auth(mut self, username: String, password: String) -> Self {
669        self.config.auth = Auth::Basic { username, password };
670        self
671    }
672
673    /// Authenticate with an OAuth token.
674    pub fn oauth(mut self, oauth: auth::OAuth) -> Self {
675        self.config.auth = Auth::OAuth(oauth);
676        self
677    }
678
679    /// Authenticate with a user access token.
680    pub fn user_access_token<S: Into<SecretString>>(mut self, token: S) -> Self {
681        self.config.auth = Auth::UserAccessToken(token.into());
682        self
683    }
684
685    /// Set the base url for `Octocrab`.
686    pub fn base_uri(mut self, base_uri: impl TryInto<Uri>) -> Result<Self> {
687        self.config.base_uri = Some(
688            base_uri
689                .try_into()
690                .map_err(|_| UriParseError {})
691                .context(UriParseSnafu)?,
692        );
693        Ok(self)
694    }
695
696    /// Set the base upload url for `Octocrab`.
697    pub fn upload_uri(mut self, upload_uri: impl TryInto<Uri>) -> Result<Self> {
698        self.config.upload_uri = Some(
699            upload_uri
700                .try_into()
701                .map_err(|_| UriParseError {})
702                .context(UriParseSnafu)?,
703        );
704        Ok(self)
705    }
706
707    pub fn cache<C>(mut self, cache: C) -> Self
708    where
709        C: CacheStorage + 'static,
710    {
711        self.config.cache_storage = Some(Arc::new(cache));
712        self
713    }
714
715    #[cfg(feature = "retry")]
716    #[cfg_attr(docsrs, doc(cfg(feature = "retry")))]
717    pub fn set_connector_retry_service<S>(
718        &self,
719        connector: hyper_util::client::legacy::Client<S, OctoBody>,
720    ) -> Retry<RetryConfig, hyper_util::client::legacy::Client<S, OctoBody>> {
721        let retry_layer = RetryLayer::new(self.config.retry_config.clone());
722
723        retry_layer.layer(connector)
724    }
725
726    #[cfg(feature = "timeout")]
727    #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
728    pub fn set_connect_timeout_service<T>(&self, connector: T) -> TimeoutConnector<T>
729    where
730        T: Service<Uri> + Send,
731        T::Response: hyper::rt::Read + hyper::rt::Write + Send + Unpin,
732        T::Future: Send + 'static,
733        T::Error: Into<BoxError>,
734    {
735        let mut connector = TimeoutConnector::new(connector);
736        // Set the timeouts for the client
737        connector.set_connect_timeout(self.config.connect_timeout);
738        connector.set_read_timeout(self.config.read_timeout);
739        connector.set_write_timeout(self.config.write_timeout);
740        connector
741    }
742
743    /// Build a [`Client`](hyper_util::client::legacy::Client) instance with the current [`Service`] stack.
744    #[cfg(feature = "default-client")]
745    #[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
746    pub fn build(self) -> Result<Octocrab> {
747        let client: hyper_util::client::legacy::Client<_, OctoBody> = {
748            #[cfg(all(not(feature = "opentls"), not(feature = "rustls")))]
749            let mut connector = hyper::client::conn::http1::HttpConnector::new();
750
751            #[cfg(all(feature = "rustls", not(feature = "opentls")))]
752            let connector = {
753                let builder = HttpsConnectorBuilder::new();
754                // Allow user to have installed a runtime default.
755                // If not, we ship with _our_ recommended default.
756                let provider = rustls::crypto::CryptoProvider::get_default()
757                    .map(|arc| arc.clone())
758                    .unwrap_or_else(default_rustls_crypto_provider);
759                #[cfg(feature = "rustls-webpki-tokio")]
760                let builder = builder
761                    .with_provider_and_webpki_roots(provider)
762                    .map_err(Into::into)
763                    .context(error::OtherSnafu)?;
764                #[cfg(not(feature = "rustls-webpki-tokio"))]
765                let builder = builder
766                    .with_provider_and_native_roots(provider)
767                    .map_err(Into::into)
768                    .context(error::OtherSnafu)?; // enabled the `rustls-native-certs` feature in hyper-rustls
769
770                builder
771                    .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.
772                    .enable_http1()
773                    .build()
774            };
775
776            #[cfg(all(feature = "opentls", not(feature = "rustls")))]
777            let connector = HttpsConnector::new();
778
779            #[cfg(feature = "timeout")]
780            let connector = self.set_connect_timeout_service(connector);
781
782            hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
783                .build(connector)
784        };
785
786        #[cfg(feature = "retry")]
787        let client = self.set_connector_retry_service(client);
788
789        #[cfg(feature = "tracing")]
790        let client = TraceLayer::new_for_http()
791            .make_span_with(|req: &Request<OctoBody>| {
792                tracing::debug_span!(
793                    "HTTP",
794                     http.method = %req.method(),
795                     http.url = %req.uri(),
796                     http.status_code = tracing::field::Empty,
797                     otel.name = req.extensions().get::<&'static str>().unwrap_or(&"HTTP"),
798                     otel.kind = "client",
799                     otel.status_code = tracing::field::Empty,
800                )
801            })
802            .on_request(|_req: &Request<OctoBody>, _span: &Span| {
803                tracing::debug!("requesting");
804            })
805            .on_response(
806                |res: &Response<hyper::body::Incoming>, _latency: Duration, span: &Span| {
807                    let status = res.status();
808                    span.record("http.status_code", status.as_u16());
809                    if status.is_client_error() || status.is_server_error() {
810                        span.record("otel.status_code", "ERROR");
811                    }
812                },
813            )
814            // Explicitly disable `on_body_chunk`. The default does nothing.
815            .on_body_chunk(())
816            .on_eos(|_: Option<&HeaderMap>, _duration: Duration, _span: &Span| {
817                tracing::debug!("stream closed");
818            })
819            .on_failure(
820                |ec: ServerErrorsFailureClass, _latency: Duration, span: &Span| {
821                    // Called when
822                    // - Calling the inner service errored
823                    // - Polling `Body` errored
824                    // - the response was classified as failure (5xx)
825                    // - End of stream was classified as failure
826                    span.record("otel.status_code", "ERROR");
827                    match ec {
828                        ServerErrorsFailureClass::StatusCode(status) => {
829                            span.record("http.status_code", status.as_u16());
830                            tracing::error!("failed with status {}", status)
831                        }
832                        ServerErrorsFailureClass::Error(err) => {
833                            tracing::error!("failed with error {}", err)
834                        }
835                    }
836                },
837            )
838            .layer(client);
839
840        #[cfg(feature = "follow-redirect")]
841        let client = tower_http::follow_redirect::FollowRedirectLayer::new().layer(client);
842
843        let mut hmap: Vec<(HeaderName, HeaderValue)> = vec![];
844
845        // Add the user agent header required by GitHub
846        hmap.push((USER_AGENT, HeaderValue::from_str("octocrab").unwrap()));
847
848        for preview in &self.config.previews {
849            hmap.push((
850                http::header::ACCEPT,
851                HeaderValue::from_str(crate::format_preview(preview).as_str()).unwrap(),
852            ));
853        }
854
855        let (auth_header, auth_state): (Option<HeaderValue>, _) = match self.config.auth {
856            Auth::None => (None, AuthState::None),
857            Auth::Basic { username, password } => {
858                (None, AuthState::BasicAuth { username, password })
859            }
860            Auth::PersonalToken(token) => (
861                Some(format!("Bearer {}", token.expose_secret()).parse().unwrap()),
862                AuthState::None,
863            ),
864            Auth::UserAccessToken(token) => (
865                Some(format!("Bearer {}", token.expose_secret()).parse().unwrap()),
866                AuthState::None,
867            ),
868            Auth::App(app_auth) => (None, AuthState::App(app_auth)),
869            Auth::OAuth(device) => (
870                Some(
871                    format!(
872                        "{} {}",
873                        device.token_type,
874                        &device.access_token.expose_secret()
875                    )
876                    .parse()
877                    .unwrap(),
878                ),
879                AuthState::None,
880            ),
881        };
882
883        for (key, value) in self.config.extra_headers.iter() {
884            hmap.push((
885                key.clone(),
886                HeaderValue::from_str(value.as_str())
887                    .map_err(http::Error::from)
888                    .context(HttpSnafu)?,
889            ));
890        }
891
892        let client = ExtraHeadersLayer::new(Arc::new(hmap)).layer(client);
893
894        let client = MapResponseBodyLayer::new(|body| {
895            BodyExt::map_err(body, |e| HyperSnafu.into_error(e)).boxed()
896        })
897        .layer(client);
898
899        let base_uri = self
900            .config
901            .base_uri
902            .clone()
903            .unwrap_or_else(|| Uri::from_str(GITHUB_BASE_URI).unwrap());
904
905        let upload_uri = self
906            .config
907            .upload_uri
908            .clone()
909            .unwrap_or_else(|| Uri::from_str(GITHUB_BASE_UPLOAD_URI).unwrap());
910
911        let client = BaseUriLayer::new(base_uri.clone()).layer(client);
912
913        let client = AuthHeaderLayer::new(auth_header, base_uri, upload_uri).layer(client);
914
915        let client = HttpCacheLayer::new(self.config.cache_storage.clone()).layer(client);
916
917        if let Some(executor) = self.executor {
918            return Ok(Octocrab::new_with_executor(client, auth_state, executor));
919        }
920
921        Ok(Octocrab::new(client, auth_state))
922    }
923}
924
925pub struct DefaultOctocrabBuilderConfig {
926    auth: Auth,
927    previews: Vec<&'static str>,
928    extra_headers: Vec<(HeaderName, String)>,
929    #[cfg(feature = "timeout")]
930    connect_timeout: Option<Duration>,
931    #[cfg(feature = "timeout")]
932    read_timeout: Option<Duration>,
933    #[cfg(feature = "timeout")]
934    write_timeout: Option<Duration>,
935    base_uri: Option<Uri>,
936    upload_uri: Option<Uri>,
937    #[cfg(feature = "retry")]
938    retry_config: RetryConfig,
939    cache_storage: Option<Arc<dyn CacheStorage>>,
940}
941
942impl Default for DefaultOctocrabBuilderConfig {
943    fn default() -> Self {
944        Self {
945            auth: Auth::None,
946            previews: Vec::new(),
947            extra_headers: Vec::new(),
948            #[cfg(feature = "timeout")]
949            connect_timeout: None,
950            #[cfg(feature = "timeout")]
951            read_timeout: None,
952            #[cfg(feature = "timeout")]
953            write_timeout: None,
954            base_uri: None,
955            upload_uri: None,
956            #[cfg(feature = "retry")]
957            retry_config: RetryConfig::Simple(3),
958            cache_storage: None,
959        }
960    }
961}
962
963impl DefaultOctocrabBuilderConfig {
964    pub fn new() -> Self {
965        Self::default()
966    }
967}
968
969#[derive(Debug, Clone)]
970struct CachedTokenInner {
971    expiration: Option<DateTime<Utc>>,
972    secret: SecretString,
973}
974
975impl CachedTokenInner {
976    fn new(secret: SecretString, expiration: Option<DateTime<Utc>>) -> Self {
977        Self { secret, expiration }
978    }
979
980    fn expose_secret(&self) -> &str {
981        self.secret.expose_secret()
982    }
983}
984
985/// A cached API access token (which may be None)
986pub struct CachedToken(RwLock<Option<CachedTokenInner>>);
987
988impl CachedToken {
989    fn clear(&self) {
990        *self.0.write().unwrap() = None;
991    }
992
993    /// Returns a valid token if it exists and is not expired or if there is no expiration date.
994    fn valid_token_with_buffer(&self, buffer: chrono::Duration) -> Option<SecretString> {
995        let inner = self.0.read().unwrap();
996
997        if let Some(token) = inner.as_ref() {
998            if let Some(exp) = token.expiration {
999                if exp - Utc::now() > buffer {
1000                    return Some(token.secret.clone());
1001                }
1002            } else {
1003                return Some(token.secret.clone());
1004            }
1005        }
1006
1007        None
1008    }
1009
1010    fn valid_token(&self) -> Option<SecretString> {
1011        self.valid_token_with_buffer(chrono::Duration::seconds(30))
1012    }
1013
1014    fn set<S: Into<SecretString>>(&self, token: S, expiration: Option<DateTime<Utc>>) {
1015        *self.0.write().unwrap() = Some(CachedTokenInner::new(token.into(), expiration));
1016    }
1017}
1018
1019impl fmt::Debug for CachedToken {
1020    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1021        self.0.read().unwrap().fmt(f)
1022    }
1023}
1024
1025impl fmt::Display for CachedToken {
1026    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1027        let option = self.0.read().unwrap();
1028        option
1029            .as_ref()
1030            .map(|s| s.expose_secret().fmt(f))
1031            .unwrap_or_else(|| write!(f, "<none>"))
1032    }
1033}
1034
1035impl Clone for CachedToken {
1036    fn clone(&self) -> CachedToken {
1037        CachedToken(RwLock::new(self.0.read().unwrap().clone()))
1038    }
1039}
1040
1041impl Default for CachedToken {
1042    fn default() -> CachedToken {
1043        CachedToken(RwLock::new(None))
1044    }
1045}
1046
1047/// State used for authenticate to Github
1048#[derive(Debug, Clone)]
1049pub enum AuthState {
1050    /// No state, although Auth::PersonalToken may have caused
1051    /// an Authorization HTTP header to be set to provide authentication.
1052    None,
1053    /// Basic Auth HTTP. (username:password)
1054    BasicAuth {
1055        /// The username
1056        username: String,
1057        /// The password
1058        password: String,
1059    },
1060    /// Github App authentication with the given app data
1061    App(AppAuth),
1062    /// Authentication via a Github App repo-specific installation
1063    Installation {
1064        /// The app authentication data (app ID and private key)
1065        app: AppAuth,
1066        /// The installation ID
1067        installation: InstallationId,
1068        /// The cached access token, if any
1069        token: CachedToken,
1070    },
1071    /// Access token based authentication.
1072    AccessToken {
1073        /// The access token
1074        token: SecretString,
1075    },
1076}
1077
1078pub type OctocrabService = Buffer<
1079    http::Request<OctoBody>,
1080    <BoxService<http::Request<OctoBody>, http::Response<BoxBody<Bytes, Error>>, BoxError> as tower::Service<http::Request<OctoBody>>>::Future
1081>;
1082
1083/// The GitHub API client.
1084#[derive(Clone)]
1085pub struct Octocrab {
1086    client: OctocrabService,
1087    auth_state: AuthState,
1088}
1089
1090impl fmt::Debug for Octocrab {
1091    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1092        f.debug_struct("Octocrab")
1093            .field("auth_state", &self.auth_state)
1094            .finish()
1095    }
1096}
1097
1098/// Defaults for Octocrab:
1099/// - `base_uri`: `https://api.github.com`
1100/// - `auth`: `None`
1101/// - `client`: http client with the `octocrab` user agent.
1102#[cfg(feature = "default-client")]
1103#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
1104impl Default for Octocrab {
1105    fn default() -> Self {
1106        OctocrabBuilder::default().build().unwrap()
1107    }
1108}
1109
1110/// # Constructors
1111impl Octocrab {
1112    /// Returns a new `OctocrabBuilder`.
1113    pub fn builder() -> OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady>
1114    {
1115        OctocrabBuilder::new_empty().with_config(DefaultOctocrabBuilderConfig::default())
1116    }
1117
1118    /// Creates a new `Octocrab`.
1119    fn new<S>(service: S, auth_state: AuthState) -> Self
1120    where
1121        S: Service<Request<OctoBody>, Response = Response<BoxBody<Bytes, crate::Error>>>
1122            + Send
1123            + 'static,
1124        S::Future: Send + 'static,
1125        S::Error: Into<BoxError>,
1126    {
1127        let service = Buffer::new(BoxService::new(service.map_err(Into::into)), 1024);
1128
1129        Self {
1130            client: service,
1131            auth_state,
1132        }
1133    }
1134
1135    /// Creates a new `Octocrab` with a custom executor
1136    fn new_with_executor<S>(service: S, auth_state: AuthState, executor: Executor) -> Self
1137    where
1138        S: Service<Request<OctoBody>, Response = Response<BoxBody<Bytes, crate::Error>>>
1139            + Send
1140            + 'static,
1141        S::Future: Send + 'static,
1142        S::Error: Into<BoxError>,
1143    {
1144        // Use Buffer pair to return the background worker
1145        let (service, worker) = Buffer::pair(BoxService::new(service.map_err(Into::into)), 1024);
1146
1147        // Execute the background worker with the custom executor
1148        executor(Box::pin(worker));
1149
1150        Self {
1151            client: service,
1152            auth_state,
1153        }
1154    }
1155
1156    /// Returns a new `Octocrab` based on the current builder but
1157    /// authorizing via a specific installation ID.
1158    /// Typically you will first construct an `Octocrab` using
1159    /// `OctocrabBuilder::app` to authenticate as your Github App,
1160    /// then obtain an installation ID, and then pass that here to
1161    /// obtain a new `Octocrab` with which you can make API calls
1162    /// with the permissions of that installation.
1163    pub fn installation(&self, id: InstallationId) -> Result<Octocrab> {
1164        let app_auth = if let AuthState::App(ref app_auth) = self.auth_state {
1165            app_auth.clone()
1166        } else {
1167            return Err(Error::Installation {
1168                backtrace: Backtrace::capture(),
1169            });
1170        };
1171        Ok(Octocrab {
1172            client: self.client.clone(),
1173            auth_state: AuthState::Installation {
1174                app: app_auth,
1175                installation: id,
1176                token: CachedToken::default(),
1177            },
1178        })
1179    }
1180
1181    /// Similar to `installation`, but also eagerly caches the installation
1182    /// token and returns the token. The returned token can be used to make
1183    /// https git requests to e.g. clone repositories that the installation
1184    /// has access to.
1185    ///
1186    /// See also <https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#http-based-git-access-by-an-installation>
1187    pub async fn installation_and_token(
1188        &self,
1189        id: InstallationId,
1190    ) -> Result<(Octocrab, SecretString)> {
1191        let crab = self.installation(id)?;
1192        let token = crab.request_installation_auth_token().await?;
1193        Ok((crab, token))
1194    }
1195
1196    /// Acquire a GitHub App installation access token that does not expire for
1197    /// at least 30 seconds. A cached token will be used if its expiration is
1198    /// far enough in the future. Otherwise, a new token will be acquired and
1199    /// cached.
1200    pub async fn installation_token(&self) -> Result<SecretString> {
1201        self.installation_token_with_buffer(chrono::Duration::seconds(30))
1202            .await
1203    }
1204
1205    /// Acquire a GitHub App installation access token that does not expire for
1206    /// at least the duration specified by [`buffer`]. A cached token will be
1207    /// used if its expiration is far enough in the future. Otherwise, a new
1208    /// token will be acquired and cached.
1209    pub async fn installation_token_with_buffer(
1210        &self,
1211        buffer: chrono::Duration,
1212    ) -> Result<SecretString> {
1213        let token = if let AuthState::Installation { ref token, .. } = self.auth_state {
1214            token
1215        } else {
1216            return Err(Error::InstallationTokenInvalidAuth {
1217                backtrace: Backtrace::capture(),
1218            });
1219        };
1220
1221        let token = match token.valid_token_with_buffer(buffer) {
1222            Some(token) => token,
1223            None => self.request_installation_auth_token().await?,
1224        };
1225
1226        Ok(token)
1227    }
1228
1229    /// Returns a new `Octocrab` based on the current builder but
1230    /// authorizing via an access token.
1231    ///
1232    /// 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>
1233    pub fn user_access_token<S: Into<SecretString>>(&self, token: S) -> Result<Self> {
1234        Ok(Octocrab {
1235            client: self.client.clone(),
1236            auth_state: AuthState::AccessToken {
1237                token: token.into(),
1238            },
1239        })
1240    }
1241}
1242
1243/// # GitHub API Methods
1244impl Octocrab {
1245    /// Creates a new [`actions::ActionsHandler`] for accessing information from
1246    /// GitHub Actions.
1247    pub fn actions(&self) -> actions::ActionsHandler<'_> {
1248        actions::ActionsHandler::new(self)
1249    }
1250
1251    /// Creates a [`current::CurrentAuthHandler`] that allows you to access
1252    /// information about the current authenticated user.
1253    pub fn current(&self) -> current::CurrentAuthHandler<'_> {
1254        current::CurrentAuthHandler::new(self)
1255    }
1256
1257    /// Creates a [`activity::ActivityHandler`] for the current authenticated user.
1258    pub fn activity(&self) -> activity::ActivityHandler<'_> {
1259        activity::ActivityHandler::new(self)
1260    }
1261
1262    /// Creates a new [`apps::AppsRequestHandler`] for the currently authenticated app.
1263    pub fn apps(&self) -> apps::AppsRequestHandler<'_> {
1264        apps::AppsRequestHandler::new(self)
1265    }
1266
1267    /// Creates a [`gitignore::GitignoreHandler`] for accessing information
1268    /// about `gitignore`.
1269    pub fn gitignore(&self) -> gitignore::GitignoreHandler<'_> {
1270        gitignore::GitignoreHandler::new(self)
1271    }
1272
1273    /// Creates a [`issues::IssueHandler`] for the repo specified at `owner/repo`,
1274    /// that allows you to access GitHub's issues API.
1275    pub fn issues(
1276        &self,
1277        owner: impl Into<String>,
1278        repo: impl Into<String>,
1279    ) -> issues::IssueHandler<'_> {
1280        issues::IssueHandler::new(self, RepoRef::ByOwnerAndName(owner.into(), repo.into()))
1281    }
1282
1283    /// Creates a [`issues::IssueHandler`] for the repo specified at repository ID,
1284    /// that allows you to access GitHub's issues API.
1285    pub fn issues_by_id(&self, id: impl Into<RepositoryId>) -> issues::IssueHandler<'_> {
1286        issues::IssueHandler::new(self, RepoRef::ById(id.into()))
1287    }
1288
1289    /// Creates a [`code_scannings::CodeScanningHandler`] for the repo specified at `owner/repo`,
1290    /// that allows you to access GitHub's Code scanning API.
1291    pub fn code_scannings(
1292        &self,
1293        owner: impl Into<String>,
1294        repo: impl Into<String>,
1295    ) -> code_scannings::CodeScanningHandler<'_> {
1296        code_scannings::CodeScanningHandler::new(self, owner.into(), Option::from(repo.into()))
1297    }
1298
1299    /// Creates a [`code_scannings::CodeScanningHandler`] for the org specified at `owner`,
1300    /// that allows you to access GitHub's Code scanning API.
1301    pub fn code_scannings_organisation(
1302        &self,
1303        owner: impl Into<String>,
1304    ) -> code_scannings::CodeScanningHandler<'_> {
1305        code_scannings::CodeScanningHandler::new(self, owner.into(), None)
1306    }
1307
1308    /// Creates a [`commits::CommitHandler`] for the repo specified at `owner/repo`,
1309    pub fn commits(
1310        &self,
1311        owner: impl Into<String>,
1312        repo: impl Into<String>,
1313    ) -> commits::CommitHandler<'_> {
1314        commits::CommitHandler::new(self, owner.into(), repo.into())
1315    }
1316
1317    /// Creates a [`licenses::LicenseHandler`].
1318    pub fn licenses(&self) -> licenses::LicenseHandler<'_> {
1319        licenses::LicenseHandler::new(self)
1320    }
1321
1322    /// Creates a [`markdown::MarkdownHandler`].
1323    pub fn markdown(&self) -> markdown::MarkdownHandler<'_> {
1324        markdown::MarkdownHandler::new(self)
1325    }
1326
1327    /// Creates an [`orgs::OrgHandler`] for the specified organization,
1328    /// that allows you to access GitHub's organization API.
1329    pub fn orgs(&self, owner: impl Into<String>) -> orgs::OrgHandler<'_> {
1330        orgs::OrgHandler::new(self, owner.into())
1331    }
1332
1333    /// Creates a [`pulls::PullRequestHandler`] for the repo specified at
1334    /// `owner/repo`, that allows you to access GitHub's pull request API.
1335    pub fn pulls(
1336        &self,
1337        owner: impl Into<String>,
1338        repo: impl Into<String>,
1339    ) -> pulls::PullRequestHandler<'_> {
1340        pulls::PullRequestHandler::new(self, owner.into(), repo.into())
1341    }
1342
1343    /// Creates a [`repos::RepoHandler`] for the repo specified at `owner/repo`,
1344    /// that allows you to access GitHub's repository API.
1345    pub fn repos(
1346        &self,
1347        owner: impl Into<String>,
1348        repo: impl Into<String>,
1349    ) -> repos::RepoHandler<'_> {
1350        repos::RepoHandler::new(self, RepoRef::ByOwnerAndName(owner.into(), repo.into()))
1351    }
1352
1353    /// Creates a [`repos::RepoHandler`] for the repo specified at repository ID,
1354    /// that allows you to access GitHub's repository API.
1355    pub fn repos_by_id(&self, id: impl Into<RepositoryId>) -> repos::RepoHandler<'_> {
1356        repos::RepoHandler::new(self, RepoRef::ById(id.into()))
1357    }
1358
1359    /// Creates a [`projects::ProjectHandler`] that allows you to access GitHub's
1360    /// projects API (classic).
1361    pub fn projects(&self) -> projects::ProjectHandler<'_> {
1362        projects::ProjectHandler::new(self)
1363    }
1364
1365    /// Creates a [`search::SearchHandler`] that allows you to construct general queries
1366    /// to GitHub's API.
1367    pub fn search(&self) -> search::SearchHandler<'_> {
1368        search::SearchHandler::new(self)
1369    }
1370
1371    /// Creates a [`teams::TeamHandler`] for the specified organization that allows
1372    /// you to access GitHub's teams API.
1373    pub fn teams(&self, owner: impl Into<String>) -> teams::TeamHandler<'_> {
1374        teams::TeamHandler::new(self, owner.into())
1375    }
1376
1377    /// Creates a [`users::UserHandler`] for the specified user using the user name
1378    pub fn users(&self, user: impl Into<String>) -> users::UserHandler<'_> {
1379        users::UserHandler::new(self, UserRef::ByString(user.into()))
1380    }
1381
1382    /// Creates a [`users::UserHandler`] for the specified user using the user ID
1383    pub fn users_by_id(&self, user: impl Into<UserId>) -> users::UserHandler<'_> {
1384        users::UserHandler::new(self, UserRef::ById(user.into()))
1385    }
1386
1387    /// Creates a [`workflows::WorkflowsHandler`] for the specified repository that allows
1388    /// you to access GitHub's workflows API.
1389    pub fn workflows(
1390        &self,
1391        owner: impl Into<String>,
1392        repo: impl Into<String>,
1393    ) -> workflows::WorkflowsHandler<'_> {
1394        workflows::WorkflowsHandler::new(self, owner.into(), repo.into())
1395    }
1396
1397    /// Creates an [`events::EventsBuilder`] that allows you to access
1398    /// GitHub's events API.
1399    pub fn events(&self) -> events::EventsBuilder<'_> {
1400        events::EventsBuilder::new(self)
1401    }
1402
1403    /// Creates a [`gists::GistsHandler`] that allows you to access
1404    /// GitHub's Gists API.
1405    pub fn gists(&self) -> gists::GistsHandler<'_> {
1406        gists::GistsHandler::new(self)
1407    }
1408
1409    /// Creates a [`checks::ChecksHandler`] that allows to access the Checks API.
1410    pub fn checks(
1411        &self,
1412        owner: impl Into<String>,
1413        repo: impl Into<String>,
1414    ) -> checks::ChecksHandler<'_> {
1415        checks::ChecksHandler::new(self, owner.into(), repo.into())
1416    }
1417
1418    /// Creates a [`ratelimit::RateLimitHandler`] that returns the API rate limit.
1419    pub fn ratelimit(&self) -> ratelimit::RateLimitHandler<'_> {
1420        ratelimit::RateLimitHandler::new(self)
1421    }
1422
1423    /// Creates a [`hooks::HooksHandler`] that returns the API hooks
1424    pub fn hooks(&self, owner: impl Into<String>) -> hooks::HooksHandler<'_> {
1425        hooks::HooksHandler::new(self, owner.into())
1426    }
1427
1428    /// Creates a [`classroom::AssignmentsHandler`] providing the GitHub Classroom _Assignments_ API
1429    pub fn assignments(&self) -> classroom::AssignmentsHandler<'_> {
1430        classroom::AssignmentsHandler::new(self)
1431    }
1432
1433    /// Creates a [`classroom::ClassroomHandler`] providing the GitHub Classroom _Classrooms_ API
1434    pub fn classrooms(&self) -> classroom::ClassroomHandler<'_> {
1435        classroom::ClassroomHandler::new(self)
1436    }
1437
1438    /// Creates a [`codes_of_conduct::CodesOfConductHandler`] providing the GitHub Codes of Codes of Conduct API
1439    pub fn codes_of_conduct(&self) -> codes_of_conduct::CodesOfConductHandler<'_> {
1440        codes_of_conduct::CodesOfConductHandler::new(self)
1441    }
1442}
1443
1444/// # GraphQL API.
1445impl Octocrab {
1446    /// Sends a graphql query to GitHub, and deserialises the response
1447    /// from JSON.
1448    /// ```no_run
1449    ///# async fn run() -> octocrab::Result<()> {
1450    /// let response: octocrab::GraphqlResponse<serde_json::Value> = octocrab::instance()
1451    ///     .graphql(&serde_json::json!({ "query": "{ viewer { login }}" }))
1452    ///     .await?;
1453    ///# Ok(())
1454    ///# }
1455    /// ```
1456    pub async fn graphql<R: serde::de::DeserializeOwned>(
1457        &self,
1458        payload: &(impl serde::Serialize + ?Sized),
1459    ) -> crate::Result<R> {
1460        let response: GraphqlResponse<R> = self
1461            .post("/graphql", Some(&serde_json::json!(payload)))
1462            .await?;
1463
1464        match response {
1465            GraphqlResponse::Ok(res) => Ok(res.data),
1466            GraphqlResponse::Err(errors) => Err(error::Error::Graphql {
1467                source: errors.errors.into(),
1468                backtrace: Backtrace::capture(),
1469            }),
1470        }
1471    }
1472}
1473
1474/// GraphQL Response.
1475/// GraphQL can return a response with `data` or `errors`, or both in the case of a partial success.
1476#[derive(Serialize, Deserialize, Debug)]
1477#[serde(untagged)]
1478pub enum GraphqlResponse<T> {
1479    /// A response containing errors.
1480    Err(GraphqlErrorResponse<T>),
1481    /// A response representing a complete success with no errors.
1482    Ok(GraphqlOkResponse<T>),
1483}
1484
1485#[derive(Serialize, Deserialize, Debug)]
1486pub struct GraphqlOkResponse<T> {
1487    pub data: T,
1488}
1489
1490#[derive(Serialize, Deserialize, Debug)]
1491pub struct GraphqlErrorResponse<T> {
1492    /// GraphQL returns `data` even in the case of a partial success.
1493    pub data: Option<T>,
1494    /// A list of errors encountered during the request.
1495    pub errors: Vec<GraphqlError>,
1496}
1497
1498/// An individual GraphQL error.
1499/// Following the [GraphQL October 2021 Spec](https://spec.graphql.org/October2021/#sec-Errors).
1500#[derive(Serialize, Deserialize, Debug)]
1501pub struct GraphqlError {
1502    /// A description of the error intended for the developer as a guide to understand and correct the error.
1503    pub message: String,
1504    /// A particular point of reference in the GraphQL query where the error occurred.
1505    /// This may be `None` if the error cannot be associated with a particular point
1506    pub locations: Option<Vec<GraphqlErrorLocation>>,
1507    /// The path to the specific field that caused the error.
1508    /// This may be `None` if the error is not associated with a specific field
1509    pub path: Option<Vec<GraphqlPathSegment>>,
1510    /// Additional error metadata provided by the server.
1511    pub extensions: Option<serde_json::Value>,
1512}
1513
1514#[derive(Serialize, Deserialize, Debug)]
1515pub struct GraphqlErrorLocation {
1516    pub line: u32,
1517    pub column: u32,
1518}
1519
1520/// A path can consist of field names (Strings) and list indices (usize).
1521#[derive(Serialize, Deserialize, Debug)]
1522#[serde(untagged)]
1523pub enum GraphqlPathSegment {
1524    Path(String),
1525    Position(usize),
1526}
1527
1528/// # HTTP Methods
1529/// A collection of different of HTTP methods to use with Octocrab's
1530/// configuration (Authenication, etc.). All of the HTTP methods (`get`, `post`,
1531/// etc.) perform some amount of pre-processing such as making relative urls
1532/// absolute, and post processing such as mapping any potential GitHub errors
1533/// into `Err()` variants, and deserializing the response body.
1534///
1535/// This isn't always ideal when working with GitHub's API and as such there are
1536/// additional methods available prefixed with `_` (e.g.  `_get`, `_post`,
1537/// etc.) that perform no pre or post processing and directly return the
1538/// `http::Response` struct.
1539impl Octocrab {
1540    /// Send a `POST` request to `route` with an optional body, returning the body
1541    /// of the response.
1542    pub async fn post<P: Serialize + ?Sized, R: FromResponse>(
1543        &self,
1544        route: impl AsRef<str>,
1545        body: Option<&P>,
1546    ) -> Result<R> {
1547        let response = self
1548            ._post(self.parameterized_uri(route, None::<&()>)?, body)
1549            .await?;
1550        R::from_response(crate::map_github_error(response).await?).await
1551    }
1552
1553    /// Send a `POST` request with no additional pre/post-processing.
1554    pub async fn _post<P: Serialize + ?Sized>(
1555        &self,
1556        uri: impl TryInto<http::Uri>,
1557        body: Option<&P>,
1558    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1559        let uri = uri
1560            .try_into()
1561            .map_err(|_| UriParseError {})
1562            .context(UriParseSnafu)?;
1563        let request = Builder::new().method(Method::POST).uri(uri);
1564        let request = self.build_request(request, body)?;
1565        self.execute(request).await
1566    }
1567
1568    /// Send a `GET` request to `route` with optional query parameters, returning
1569    /// the body of the response.
1570    pub async fn get<R, A, P>(&self, route: A, parameters: Option<&P>) -> Result<R>
1571    where
1572        A: AsRef<str>,
1573        P: Serialize + ?Sized,
1574        R: FromResponse,
1575    {
1576        self.get_with_headers(route, parameters, None).await
1577    }
1578
1579    /// Send a `GET` request with no additional post-processing.
1580    pub async fn _get(
1581        &self,
1582        uri: impl TryInto<Uri>,
1583    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1584        self._get_with_headers(uri, None).await
1585    }
1586
1587    /// Convenience method to accept any &str, and attempt to convert it to a Uri.
1588    /// the method also attempts to serialize any parameters into a query string, and append it to the uri.
1589    pub(crate) fn parameterized_uri<A, P>(&self, uri: A, parameters: Option<&P>) -> Result<Uri>
1590    where
1591        A: AsRef<str>,
1592        P: Serialize + ?Sized,
1593    {
1594        let mut uri = uri.as_ref().to_string();
1595        if let Some(parameters) = parameters {
1596            if uri.contains('?') {
1597                uri = format!("{uri}&");
1598            } else {
1599                uri = format!("{uri}?");
1600            }
1601            uri = format!(
1602                "{}{}",
1603                uri,
1604                serde_urlencoded::to_string(parameters)
1605                    .context(SerdeUrlEncodedSnafu)?
1606                    .as_str()
1607            );
1608        }
1609        let uri = Uri::from_str(uri.as_str()).context(UriSnafu);
1610        uri
1611    }
1612
1613    pub async fn body_to_string(
1614        &self,
1615        res: http::Response<BoxBody<Bytes, crate::Error>>,
1616    ) -> Result<String> {
1617        let body_bytes = res.into_body().collect().await?.to_bytes();
1618        String::from_utf8(body_bytes.to_vec()).context(InvalidUtf8Snafu)
1619    }
1620
1621    /// Send a `GET` request to `route` with optional query parameters and headers, returning
1622    /// the body of the response.
1623    pub async fn get_with_headers<R, A, P>(
1624        &self,
1625        route: A,
1626        parameters: Option<&P>,
1627        headers: Option<http::header::HeaderMap>,
1628    ) -> Result<R>
1629    where
1630        A: AsRef<str>,
1631        P: Serialize + ?Sized,
1632        R: FromResponse,
1633    {
1634        let response = self
1635            ._get_with_headers(self.parameterized_uri(route, parameters)?, headers)
1636            .await?;
1637        R::from_response(crate::map_github_error(response).await?).await
1638    }
1639
1640    /// Send a `GET` request including option to set headers, with no additional post-processing.
1641    pub async fn _get_with_headers(
1642        &self,
1643        uri: impl TryInto<Uri>,
1644        headers: Option<http::header::HeaderMap>,
1645    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1646        let uri = uri
1647            .try_into()
1648            .map_err(|_| UriParseError {})
1649            .context(UriParseSnafu)?;
1650        let mut request = Builder::new().method(Method::GET).uri(uri);
1651        if let Some(headers) = headers {
1652            for (key, value) in headers.iter() {
1653                request = request.header(key, value);
1654            }
1655        }
1656        let request = self.build_request(request, None::<&()>)?;
1657        self.execute(request).await
1658    }
1659
1660    /// Send a `PATCH` request to `route` with optional query parameters,
1661    /// returning the body of the response.
1662    pub async fn patch<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1663    where
1664        A: AsRef<str>,
1665        B: Serialize + ?Sized,
1666        R: FromResponse,
1667    {
1668        let response = self
1669            ._patch(self.parameterized_uri(route, None::<&()>)?, body)
1670            .await?;
1671        R::from_response(crate::map_github_error(response).await?).await
1672    }
1673
1674    /// Send a `PATCH` request with no additional post-processing.
1675    pub async fn _patch<B: Serialize + ?Sized>(
1676        &self,
1677        uri: impl TryInto<Uri>,
1678        body: Option<&B>,
1679    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1680        let uri = uri
1681            .try_into()
1682            .map_err(|_| UriParseError {})
1683            .context(UriParseSnafu)?;
1684        let request = Builder::new().method(Method::PATCH).uri(uri);
1685        let request = self.build_request(request, body)?;
1686        self.execute(request).await
1687    }
1688
1689    /// Send a `PUT` request to `route` with optional query parameters,
1690    /// returning the body of the response.
1691    pub async fn put<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1692    where
1693        A: AsRef<str>,
1694        B: Serialize + ?Sized,
1695        R: FromResponse,
1696    {
1697        let response = self
1698            ._put(self.parameterized_uri(route, None::<&()>)?, body)
1699            .await?;
1700        R::from_response(crate::map_github_error(response).await?).await
1701    }
1702
1703    /// Send a `PUT` request with no additional post-processing.
1704    pub async fn _put<B: Serialize + ?Sized>(
1705        &self,
1706        uri: impl TryInto<Uri>,
1707        body: Option<&B>,
1708    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1709        let uri = uri
1710            .try_into()
1711            .map_err(|_| UriParseError {})
1712            .context(UriParseSnafu)?;
1713        let request = Builder::new().method(Method::PUT).uri(uri);
1714        let request = self.build_request(request, body)?;
1715        self.execute(request).await
1716    }
1717
1718    pub fn build_request<B: Serialize + ?Sized>(
1719        &self,
1720        mut builder: Builder,
1721        body: Option<&B>,
1722    ) -> Result<http::Request<OctoBody>> {
1723        // Since Octocrab doesn't require streamable bodies(aka, file upload) because it is serde::Serialize),
1724        // we can just use String body, since it is both http_body::Body(required by Hyper::Client), and Clone(required by BoxService).
1725
1726        // In case octocrab needs to support cases where body is strictly streamable, it should use something like reqwest::Body,
1727        // since it differentiates between retryable bodies, and streams(aka, it implements try_clone(), which is needed for middlewares like retry).
1728
1729        // Add headers specified in Cargo.toml
1730        // '[package.metadata.github-api].request-headers' section
1731        for kv in _SET_HEADERS_MAP {
1732            builder = builder.header(kv.0, kv.1);
1733        }
1734
1735        if let Some(body) = body {
1736            builder = builder.header(http::header::CONTENT_TYPE, "application/json");
1737            let serialized = serde_json::to_string(body).context(SerdeSnafu)?;
1738            let body: OctoBody = serialized.into();
1739            let request = builder.body(body).context(HttpSnafu)?;
1740            Ok(request)
1741        } else {
1742            Ok(builder
1743                .header(http::header::CONTENT_LENGTH, "0")
1744                .body(OctoBody::empty())
1745                .context(HttpSnafu)?)
1746        }
1747    }
1748
1749    /// Send a `DELETE` request to `route` with optional query body,
1750    /// returning the body of the response.
1751    pub async fn delete<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1752    where
1753        A: AsRef<str>,
1754        B: Serialize + ?Sized,
1755        R: FromResponse,
1756    {
1757        let response = self
1758            ._delete(self.parameterized_uri(route, None::<&()>)?, body)
1759            .await?;
1760        R::from_response(crate::map_github_error(response).await?).await
1761    }
1762
1763    /// Send a `DELETE` request with no additional post-processing.
1764    pub async fn _delete<B: Serialize + ?Sized>(
1765        &self,
1766        uri: impl TryInto<Uri>,
1767        body: Option<&B>,
1768    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1769        let uri = uri
1770            .try_into()
1771            .map_err(|_| UriParseError {})
1772            .context(UriParseSnafu)?;
1773        let request = self.build_request(Builder::new().method(Method::DELETE).uri(uri), body)?;
1774
1775        self.execute(request).await
1776    }
1777
1778    /// Requests a fresh installation auth token and caches it. Returns the token.
1779    async fn request_installation_auth_token(&self) -> Result<SecretString> {
1780        let (app, installation, token) = if let AuthState::Installation {
1781            ref app,
1782            installation,
1783            ref token,
1784        } = self.auth_state
1785        {
1786            (app, installation, token)
1787        } else {
1788            return Err(Error::Installation {
1789                backtrace: Backtrace::capture(),
1790            });
1791        };
1792        let mut request = Builder::new();
1793        let mut sensitive_value =
1794            HeaderValue::from_str(format!("Bearer {}", app.generate_bearer_token()?).as_str())
1795                .map_err(http::Error::from)
1796                .context(HttpSnafu)?;
1797
1798        let uri = http::Uri::builder()
1799            .path_and_query(format!("/app/installations/{installation}/access_tokens"))
1800            .build()
1801            .context(HttpSnafu)?;
1802
1803        sensitive_value.set_sensitive(true);
1804        request = request
1805            .header(http::header::AUTHORIZATION, sensitive_value)
1806            .method(http::Method::POST)
1807            .uri(uri);
1808        let response = self
1809            .send(request.body("{}".into()).context(HttpSnafu)?)
1810            .await?;
1811        let _status = response.status();
1812
1813        let token_object =
1814            InstallationToken::from_response(crate::map_github_error(response).await?).await?;
1815
1816        let expiration = token_object
1817            .expires_at
1818            .map(|time| {
1819                DateTime::<Utc>::from_str(&time).map_err(|e| error::Error::Other {
1820                    source: Box::new(e),
1821                    backtrace: snafu::Backtrace::capture(),
1822                })
1823            })
1824            .transpose()?;
1825
1826        #[cfg(feature = "tracing")]
1827        tracing::debug!("Token expires at: {:?}", expiration);
1828
1829        token.set(token_object.token.clone(), expiration);
1830
1831        Ok(SecretString::from(token_object.token))
1832    }
1833
1834    /// Send the given request to the underlying service
1835    pub async fn send(
1836        &self,
1837        request: Request<OctoBody>,
1838    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1839        let mut svc = self.client.clone();
1840        let response: Response<BoxBody<Bytes, crate::Error>> = svc
1841            .ready()
1842            .await
1843            .context(ServiceSnafu)?
1844            .call(request)
1845            .await
1846            .context(ServiceSnafu)?;
1847        Ok(response)
1848        //todo: attempt to downcast error to something more specific before returning. (Currently having trouble with this because I am not accustomed with snafu)
1849        // map_err(|err| {
1850        //     // Error decorating request
1851        //     err.downcast::<Error>()
1852        //         .map(|e| *e)
1853        //         // Error requesting
1854        //         .or_else(|err| err.downcast::<hyper::Error>().map(|err| Error::HyperError(*err)))
1855        //         // Error from another middleware
1856        //         .unwrap_or_else(|err| Error::Service(err))
1857        // })?;
1858    }
1859
1860    /// Execute the given `request` using octocrab's Client.
1861    pub async fn execute(
1862        &self,
1863        request: http::Request<impl Into<OctoBody>>,
1864    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1865        let (mut parts, body) = request.into_parts();
1866        let body: OctoBody = body.into();
1867        // Saved request that we can retry later if necessary
1868        let auth_header: Option<HeaderValue> = match self.auth_state {
1869            AuthState::None => None,
1870            AuthState::App(ref app) => Some(
1871                HeaderValue::from_str(format!("Bearer {}", app.generate_bearer_token()?).as_str())
1872                    .map_err(http::Error::from)
1873                    .context(HttpSnafu)?,
1874            ),
1875            AuthState::BasicAuth {
1876                ref username,
1877                ref password,
1878            } => {
1879                // Equivalent implementation of: https://github.com/seanmonstar/reqwest/blob/df2b3baadc1eade54b1c22415792b778442673a4/src/util.rs#L3-L23
1880                use base64::prelude::BASE64_STANDARD;
1881                use base64::write::EncoderWriter;
1882
1883                let mut buf = b"Basic ".to_vec();
1884                {
1885                    let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
1886                    write!(encoder, "{username}:{password}").expect("writing to a Vec never fails");
1887                }
1888                Some(HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"))
1889            }
1890            AuthState::Installation { ref token, .. } => {
1891                let token = if let Some(token) = token.valid_token() {
1892                    token
1893                } else {
1894                    self.request_installation_auth_token().await?
1895                };
1896
1897                Some(
1898                    HeaderValue::from_str(format!("Bearer {}", token.expose_secret()).as_str())
1899                        .map_err(http::Error::from)
1900                        .context(HttpSnafu)?,
1901                )
1902            }
1903            AuthState::AccessToken { ref token } => Some(
1904                HeaderValue::from_str(format!("Bearer {}", token.expose_secret()).as_str())
1905                    .map_err(http::Error::from)
1906                    .context(HttpSnafu)?,
1907            ),
1908        };
1909
1910        if let Some(mut auth_header) = auth_header {
1911            // Only set the auth_header if the authority (host) is api.github.com or empty (destined for
1912            // GitHub). Otherwise, leave it off as we could have been redirected
1913            // away from GitHub (via follow_location_to_data()), and we don't
1914            // want to give our credentials to third-party services.
1915            match parts.uri.authority() {
1916                None => {
1917                    auth_header.set_sensitive(true);
1918                    parts
1919                        .headers
1920                        .insert(http::header::AUTHORIZATION, auth_header);
1921                }
1922                Some(authority) if authority == "api.github.com" => {
1923                    auth_header.set_sensitive(true);
1924                    parts
1925                        .headers
1926                        .insert(http::header::AUTHORIZATION, auth_header);
1927                }
1928                Some(_) => {
1929                    // Don't insert auth header.
1930                }
1931            }
1932        }
1933
1934        let request = http::Request::from_parts(parts, body);
1935
1936        let response = self.send(request).await?;
1937
1938        let status = response.status();
1939        if StatusCode::UNAUTHORIZED == status {
1940            if let AuthState::Installation { ref token, .. } = self.auth_state {
1941                token.clear();
1942            }
1943        }
1944        Ok(response)
1945    }
1946
1947    pub async fn follow_location_to_data(
1948        &self,
1949        response: http::Response<BoxBody<Bytes, Error>>,
1950    ) -> crate::Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1951        if let Some(redirect) = response.headers().get(http::header::LOCATION) {
1952            let location = redirect.to_str().expect("Location URL not valid str");
1953
1954            self._get(location).await
1955        } else {
1956            Ok(response)
1957        }
1958    }
1959
1960    /// Download a file from the given URL with the given content type
1961    ///
1962    /// This is a convenience method that sets the `Accept` header to the given
1963    /// content type and downloads the file into a `Vec<u8>`.
1964    pub async fn download(
1965        &self,
1966        uri: impl TryInto<Uri>,
1967        content_type: impl TryInto<http::HeaderValue>,
1968    ) -> crate::Result<Vec<u8>> {
1969        let uri = uri
1970            .try_into()
1971            .map_err(|_| UriParseError {})
1972            .context(UriParseSnafu)?;
1973        let content_type = content_type
1974            .try_into()
1975            .map_err(|_| UriParseError {})
1976            .context(UriParseSnafu)?;
1977
1978        let mut request = Builder::new().method(Method::GET).uri(uri);
1979        request = request.header(http::header::ACCEPT, content_type);
1980
1981        let request = self.build_request(request, None::<&()>)?;
1982        let response = self.execute(request).await?;
1983
1984        let bytes = response.into_body().collect().await?.to_bytes();
1985        Ok(bytes.to_vec())
1986    }
1987
1988    /// Download a zip file from the given URL into a `Vec<u8>`.
1989    pub async fn download_zip(&self, uri: impl TryInto<Uri>) -> crate::Result<Vec<u8>> {
1990        self.download(uri, "application/zip").await
1991    }
1992}
1993
1994/// # Utility Methods
1995impl Octocrab {
1996    /// A convenience method to get a page of results (if present).
1997    pub async fn get_page<R: serde::de::DeserializeOwned>(
1998        &self,
1999        uri: &Option<Uri>,
2000    ) -> crate::Result<Option<Page<R>>> {
2001        match uri {
2002            Some(uri) => self.get(uri.to_string(), None::<&()>).await.map(Some),
2003            None => Ok(None),
2004        }
2005    }
2006
2007    /// A convenience method to get all the results starting at a given
2008    /// page.
2009    pub async fn all_pages<R: serde::de::DeserializeOwned>(
2010        &self,
2011        mut page: Page<R>,
2012    ) -> crate::Result<Vec<R>> {
2013        let mut ret = page.take_items();
2014        while let Some(mut next_page) = self.get_page(&page.next).await? {
2015            ret.append(&mut next_page.take_items());
2016            page = next_page;
2017        }
2018        Ok(ret)
2019    }
2020}
2021
2022#[cfg(test)]
2023mod tests {
2024    // tokio runtime seems to be needed for tower: https://users.rust-lang.org/t/no-reactor-running-when-calling-runtime-spawn/81256
2025    #[tokio::test]
2026    async fn parametrize_uri_valid() {
2027        //Previously, invalid characters were handled by url lib's parse function.
2028        //Todo: should we handle encoding of uri routes ourselves?
2029        let uri = crate::instance()
2030            .parameterized_uri("/help%20world", None::<&()>)
2031            .unwrap();
2032        assert_eq!(uri.path(), "/help%20world");
2033    }
2034
2035    #[tokio::test]
2036    async fn extra_headers() {
2037        use http::header::HeaderName;
2038        use wiremock::{matchers, Mock, MockServer, ResponseTemplate};
2039        let response = ResponseTemplate::new(304).append_header("etag", "\"abcd\"");
2040        let mock_server = MockServer::start().await;
2041        Mock::given(matchers::method("GET"))
2042            .and(matchers::path_regex(".*"))
2043            .and(matchers::header("x-test1", "hello"))
2044            .and(matchers::header("x-test2", "goodbye"))
2045            .respond_with(response)
2046            .expect(1)
2047            .mount(&mock_server)
2048            .await;
2049        crate::OctocrabBuilder::default()
2050            .base_uri(mock_server.uri())
2051            .unwrap()
2052            .add_header(HeaderName::from_static("x-test1"), "hello".to_string())
2053            .add_header(HeaderName::from_static("x-test2"), "goodbye".to_string())
2054            .build()
2055            .unwrap()
2056            .repos("XAMPPRocky", "octocrab")
2057            .events()
2058            .send()
2059            .await
2060            .unwrap();
2061    }
2062
2063    use super::*;
2064    use chrono::Duration;
2065
2066    #[test]
2067    fn clear_token() {
2068        let cache = CachedToken(RwLock::new(None));
2069        cache.set("secret".to_string(), None);
2070        cache.clear();
2071
2072        assert!(cache.valid_token().is_none(), "Token was not cleared.");
2073    }
2074
2075    #[test]
2076    fn no_token_when_expired() {
2077        let cache = CachedToken(RwLock::new(None));
2078        let expiration = Utc::now() + Duration::seconds(9);
2079        cache.set("secret".to_string(), Some(expiration));
2080
2081        assert!(
2082            cache
2083                .valid_token_with_buffer(Duration::seconds(10))
2084                .is_none(),
2085            "Token should be considered expired due to buffer."
2086        );
2087    }
2088
2089    #[test]
2090    fn get_valid_token_outside_buffer() {
2091        let cache = CachedToken(RwLock::new(None));
2092        let expiration = Utc::now() + Duration::seconds(12);
2093        cache.set("secret".to_string(), Some(expiration));
2094
2095        assert!(
2096            cache
2097                .valid_token_with_buffer(Duration::seconds(10))
2098                .is_some(),
2099            "Token should still be valid outside of buffer."
2100        );
2101    }
2102
2103    #[test]
2104    fn get_valid_token_without_expiration() {
2105        let cache = CachedToken(RwLock::new(None));
2106        cache.set("secret".to_string(), None);
2107
2108        assert!(
2109            cache
2110                .valid_token_with_buffer(Duration::seconds(10))
2111                .is_some(),
2112            "Token with no expiration should always be considered valid."
2113        );
2114    }
2115}