1#![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
306pub 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
312include!(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
325pub fn format_preview(preview: impl AsRef<str>) -> String {
331 format!("application/vnd.github.{}-preview", preview.as_ref())
332}
333
334pub 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
358pub 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#[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#[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
417pub 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
440pub struct NoConfig {}
442
443pub struct NoSvc {}
445
446pub struct NotLayerReady {}
448pub struct LayerReady {}
449
450pub 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 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 pub fn build(self) -> Result<Octocrab, Infallible> {
563 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 #[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 #[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 #[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 #[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 pub fn add_preview(mut self, preview: &'static str) -> Self {
625 self.config.previews.push(preview);
626 self
627 }
628
629 pub fn add_header(mut self, key: HeaderName, value: String) -> Self {
631 self.config.extra_headers.push((key, value));
632 self
633 }
634
635 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 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 pub fn basic_auth(mut self, username: String, password: String) -> Self {
651 self.config.auth = Auth::Basic { username, password };
652 self
653 }
654
655 pub fn oauth(mut self, oauth: auth::OAuth) -> Self {
657 self.config.auth = Auth::OAuth(oauth);
658 self
659 }
660
661 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 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 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 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 #[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)?; builder
745 .https_or_http() .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 .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 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 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
959pub struct CachedToken(RwLock<Option<CachedTokenInner>>);
961
962impl CachedToken {
963 fn clear(&self) {
964 *self.0.write().unwrap() = None;
965 }
966
967 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#[derive(Debug, Clone)]
1023pub enum AuthState {
1024 None,
1027 BasicAuth {
1029 username: String,
1031 password: String,
1033 },
1034 App(AppAuth),
1036 Installation {
1038 app: AppAuth,
1040 installation: InstallationId,
1042 token: CachedToken,
1044 },
1045 AccessToken {
1047 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#[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#[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
1084impl Octocrab {
1086 pub fn builder() -> OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady>
1088 {
1089 OctocrabBuilder::new_empty().with_config(DefaultOctocrabBuilderConfig::default())
1090 }
1091
1092 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 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 let (service, worker) = Buffer::pair(BoxService::new(service.map_err(Into::into)), 1024);
1120
1121 executor(Box::pin(worker));
1123
1124 Self {
1125 client: service,
1126 auth_state,
1127 }
1128 }
1129
1130 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 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 pub async fn installation_token(&self) -> Result<SecretString> {
1175 self.installation_token_with_buffer(chrono::Duration::seconds(30))
1176 .await
1177 }
1178
1179 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 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
1217impl Octocrab {
1219 pub fn actions(&self) -> actions::ActionsHandler<'_> {
1222 actions::ActionsHandler::new(self)
1223 }
1224
1225 pub fn current(&self) -> current::CurrentAuthHandler<'_> {
1228 current::CurrentAuthHandler::new(self)
1229 }
1230
1231 pub fn activity(&self) -> activity::ActivityHandler<'_> {
1233 activity::ActivityHandler::new(self)
1234 }
1235
1236 pub fn apps(&self) -> apps::AppsRequestHandler<'_> {
1238 apps::AppsRequestHandler::new(self)
1239 }
1240
1241 pub fn gitignore(&self) -> gitignore::GitignoreHandler<'_> {
1244 gitignore::GitignoreHandler::new(self)
1245 }
1246
1247 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 pub fn issues_by_id(&self, id: impl Into<RepositoryId>) -> issues::IssueHandler<'_> {
1260 issues::IssueHandler::new(self, RepoRef::ById(id.into()))
1261 }
1262
1263 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 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 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 pub fn licenses(&self) -> licenses::LicenseHandler<'_> {
1293 licenses::LicenseHandler::new(self)
1294 }
1295
1296 pub fn markdown(&self) -> markdown::MarkdownHandler<'_> {
1298 markdown::MarkdownHandler::new(self)
1299 }
1300
1301 pub fn orgs(&self, owner: impl Into<String>) -> orgs::OrgHandler<'_> {
1304 orgs::OrgHandler::new(self, owner.into())
1305 }
1306
1307 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 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 pub fn repos_by_id(&self, id: impl Into<RepositoryId>) -> repos::RepoHandler<'_> {
1330 repos::RepoHandler::new(self, RepoRef::ById(id.into()))
1331 }
1332
1333 pub fn projects(&self) -> projects::ProjectHandler<'_> {
1336 projects::ProjectHandler::new(self)
1337 }
1338
1339 pub fn search(&self) -> search::SearchHandler<'_> {
1342 search::SearchHandler::new(self)
1343 }
1344
1345 pub fn teams(&self, owner: impl Into<String>) -> teams::TeamHandler<'_> {
1348 teams::TeamHandler::new(self, owner.into())
1349 }
1350
1351 pub fn users(&self, user: impl Into<String>) -> users::UserHandler<'_> {
1353 users::UserHandler::new(self, UserRef::ByString(user.into()))
1354 }
1355
1356 pub fn users_by_id(&self, user: impl Into<UserId>) -> users::UserHandler<'_> {
1358 users::UserHandler::new(self, UserRef::ById(user.into()))
1359 }
1360
1361 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 pub fn events(&self) -> events::EventsBuilder<'_> {
1374 events::EventsBuilder::new(self)
1375 }
1376
1377 pub fn gists(&self) -> gists::GistsHandler<'_> {
1380 gists::GistsHandler::new(self)
1381 }
1382
1383 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 pub fn ratelimit(&self) -> ratelimit::RateLimitHandler<'_> {
1394 ratelimit::RateLimitHandler::new(self)
1395 }
1396
1397 pub fn hooks(&self, owner: impl Into<String>) -> hooks::HooksHandler<'_> {
1399 hooks::HooksHandler::new(self, owner.into())
1400 }
1401
1402 pub fn assignments(&self) -> classroom::AssignmentsHandler<'_> {
1404 classroom::AssignmentsHandler::new(self)
1405 }
1406
1407 pub fn classrooms(&self) -> classroom::ClassroomHandler<'_> {
1409 classroom::ClassroomHandler::new(self)
1410 }
1411
1412 pub fn codes_of_conduct(&self) -> codes_of_conduct::CodesOfConductHandler<'_> {
1414 codes_of_conduct::CodesOfConductHandler::new(self)
1415 }
1416}
1417
1418impl Octocrab {
1420 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#[derive(Serialize, Deserialize, Debug)]
1451#[serde(untagged)]
1452pub enum GraphqlResponse<T> {
1453 Err(GraphqlErrorResponse<T>),
1455 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 pub data: Option<T>,
1468 pub errors: Vec<GraphqlError>,
1470}
1471
1472#[derive(Serialize, Deserialize, Debug)]
1475pub struct GraphqlError {
1476 pub message: String,
1478 pub locations: Option<Vec<GraphqlErrorLocation>>,
1481 pub path: Option<Vec<GraphqlPathSegment>>,
1484 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#[derive(Serialize, Deserialize, Debug)]
1496#[serde(untagged)]
1497pub enum GraphqlPathSegment {
1498 Path(String),
1499 Position(usize),
1500}
1501
1502impl Octocrab {
1514 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 }
1833
1834 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 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 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 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 }
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 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 pub async fn download_zip(&self, uri: impl TryInto<Uri>) -> crate::Result<Vec<u8>> {
1964 self.download(uri, "application/zip").await
1965 }
1966}
1967
1968impl Octocrab {
1970 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 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::test]
2000 async fn parametrize_uri_valid() {
2001 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}