1#![allow(missing_docs)] use std::fmt;
84use std::pin::Pin;
85use std::str::FromStr;
86use std::sync::{Arc, Mutex};
87use std::time;
88use std::time::{Duration, SystemTime, UNIX_EPOCH};
89
90use futures::{future, prelude::*, stream, Future as StdFuture, Stream as StdStream};
91#[cfg(feature = "httpcache")]
92use http::header::IF_NONE_MATCH;
93use http::header::{HeaderMap, HeaderValue};
94use http::header::{ACCEPT, AUTHORIZATION, ETAG, LINK, USER_AGENT};
95use http::{Method, StatusCode};
96#[cfg(feature = "httpcache")]
97use hyperx::header::LinkValue;
98use hyperx::header::{qitem, Link, RelationType};
99use jsonwebtoken as jwt;
100use log::{debug, error, trace};
101use mime::Mime;
102use reqwest::Url;
103use reqwest::{Body, Client};
104use serde::de::DeserializeOwned;
105use serde::Serialize;
106
107#[doc(hidden)] #[cfg(feature = "httpcache")]
109pub mod http_cache;
110#[macro_use]
111mod macros; pub mod activity;
113pub mod app;
114pub mod branches;
115pub mod checks;
116pub mod collaborators;
117pub mod comments;
118pub mod content;
119pub mod deployments;
120pub mod errors;
121pub mod gists;
122pub mod git;
123pub mod hooks;
124pub mod issues;
125pub mod keys;
126pub mod labels;
127pub mod membership;
128pub mod notifications;
129pub mod organizations;
130pub mod pull_commits;
131pub mod pulls;
132pub mod rate_limit;
133pub mod releases;
134pub mod repo_commits;
135pub mod repositories;
136pub mod review_comments;
137pub mod review_requests;
138pub mod search;
139pub mod stars;
140pub mod statuses;
141pub mod teams;
142pub mod traffic;
143pub mod users;
144pub mod watching;
145pub mod milestone;
146
147pub use crate::errors::{Error, Result};
148#[cfg(feature = "httpcache")]
149pub use crate::http_cache::{BoxedHttpCache, HttpCache};
150
151use crate::activity::Activity;
152use crate::app::App;
153use crate::gists::{Gists, UserGists};
154use crate::organizations::{Organization, Organizations, UserOrganizations};
155use crate::rate_limit::RateLimit;
156use crate::repositories::{OrganizationRepositories, Repositories, Repository, UserRepositories};
157use crate::search::Search;
158use crate::users::Users;
159
160const DEFAULT_HOST: &str = "https://api.github.com";
161const MAX_JWT_TOKEN_LIFE: time::Duration = time::Duration::from_secs(60 * 9);
164const JWT_TOKEN_REFRESH_PERIOD: time::Duration = time::Duration::from_secs(60 * 8);
166
167pub type Future<T> = Pin<Box<dyn StdFuture<Output = Result<T>> + Send>>;
169
170pub type Stream<T> = Pin<Box<dyn StdStream<Item = Result<T>> + Send>>;
172
173const X_GITHUB_REQUEST_ID: &str = "x-github-request-id";
174const X_RATELIMIT_LIMIT: &str = "x-ratelimit-limit";
175const X_RATELIMIT_REMAINING: &str = "x-ratelimit-remaining";
176const X_RATELIMIT_RESET: &str = "x-ratelimit-reset";
177
178pub(crate) mod utils {
179 pub use percent_encoding::percent_encode;
180 use percent_encoding::{AsciiSet, CONTROLS};
181
182 const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
184
185 pub const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
187
188 pub const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'%');
189}
190
191#[derive(Clone, Copy)]
194pub enum MediaType {
195 Json,
197 Preview(&'static str),
199}
200
201impl Default for MediaType {
202 fn default() -> MediaType {
203 MediaType::Json
204 }
205}
206
207impl From<MediaType> for Mime {
208 fn from(media: MediaType) -> Mime {
209 match media {
210 MediaType::Json => "application/vnd.github.v3+json".parse().unwrap(),
211 MediaType::Preview(codename) => {
212 format!("application/vnd.github.{}-preview+json", codename)
213 .parse()
214 .unwrap_or_else(|_| {
215 panic!("could not parse media type for preview {}", codename)
216 })
217 }
218 }
219 }
220}
221
222#[derive(Clone, Copy, Debug, PartialEq)]
224pub enum AuthenticationConstraint {
225 Unconstrained,
227 JWT,
229}
230
231#[derive(Clone, Copy, Debug, PartialEq)]
233pub enum SortDirection {
234 Asc,
236 Desc,
238}
239
240impl fmt::Display for SortDirection {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match *self {
243 SortDirection::Asc => "asc",
244 SortDirection::Desc => "desc",
245 }
246 .fmt(f)
247 }
248}
249
250impl Default for SortDirection {
251 fn default() -> SortDirection {
252 SortDirection::Asc
253 }
254}
255
256#[derive(PartialEq, Clone)]
258pub enum Credentials {
259 Token(String),
262 Client(String, String),
265 JWT(JWTCredentials),
269 InstallationToken(InstallationTokenGenerator),
272}
273
274impl fmt::Debug for Credentials {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 match self {
277 Credentials::Token(value) => f
278 .debug_tuple("Credentials::Token")
279 .field(&"*".repeat(value.len()))
280 .finish(),
281 Credentials::Client(id, secret) => f
282 .debug_tuple("Credentials::Client")
283 .field(&id)
284 .field(&"*".repeat(secret.len()))
285 .finish(),
286 Credentials::JWT(jwt) => f
287 .debug_struct("Credentials::JWT")
288 .field("app_id", &jwt.app_id)
289 .field("private_key", &"vec![***]")
290 .finish(),
291 Credentials::InstallationToken(generator) => f
292 .debug_struct("Credentials::InstallationToken")
293 .field("installation_id", &generator.installation_id)
294 .field("jwt_credential", &"***")
295 .finish(),
296 }
297 }
298}
299
300#[derive(Clone)]
309pub struct JWTCredentials {
310 pub app_id: u64,
311 pub private_key: Vec<u8>,
314 cache: Arc<Mutex<ExpiringJWTCredential>>,
315}
316
317impl JWTCredentials {
318 pub fn new(app_id: u64, private_key: Vec<u8>) -> Result<JWTCredentials> {
319 let creds = ExpiringJWTCredential::calculate(app_id, &private_key)?;
320
321 Ok(JWTCredentials {
322 app_id,
323 private_key,
324 cache: Arc::new(Mutex::new(creds)),
325 })
326 }
327
328 fn is_stale(&self) -> bool {
329 self.cache.lock().unwrap().is_stale()
330 }
331
332 pub fn token(&self) -> String {
334 let mut expiring = self.cache.lock().unwrap();
335 if expiring.is_stale() {
336 *expiring = ExpiringJWTCredential::calculate(self.app_id, &self.private_key)
337 .expect("JWT private key worked before, it should work now...");
338 }
339
340 expiring.token.clone()
341 }
342}
343
344impl PartialEq for JWTCredentials {
345 fn eq(&self, other: &JWTCredentials) -> bool {
346 self.app_id == other.app_id && self.private_key == other.private_key
347 }
348}
349
350#[derive(Debug)]
351struct ExpiringJWTCredential {
352 token: String,
353 created_at: time::Instant,
354}
355
356#[derive(Serialize)]
357struct JWTCredentialClaim {
358 iat: u64,
359 exp: u64,
360 iss: u64,
361}
362
363impl ExpiringJWTCredential {
364 fn calculate(app_id: u64, private_key: &[u8]) -> Result<ExpiringJWTCredential> {
365 let created_at = time::Instant::now();
368 let now = time::SystemTime::now()
369 .duration_since(time::UNIX_EPOCH)
370 .unwrap();
371 let expires = now + MAX_JWT_TOKEN_LIFE;
372
373 let payload = JWTCredentialClaim {
374 iat: now.as_secs(),
375 exp: expires.as_secs(),
376 iss: app_id,
377 };
378 let header = jwt::Header::new(jwt::Algorithm::RS256);
379 let jwt = jwt::encode(
380 &header,
381 &payload,
382 &jsonwebtoken::EncodingKey::from_rsa_der(private_key),
383 )?;
384
385 Ok(ExpiringJWTCredential {
386 created_at,
387 token: jwt,
388 })
389 }
390
391 fn is_stale(&self) -> bool {
392 self.created_at.elapsed() >= JWT_TOKEN_REFRESH_PERIOD
393 }
394}
395
396#[derive(Debug, Clone)]
404pub struct InstallationTokenGenerator {
405 pub installation_id: u64,
406 pub jwt_credential: Box<Credentials>,
407 access_key: Arc<Mutex<Option<String>>>,
408}
409
410impl InstallationTokenGenerator {
411 pub fn new(installation_id: u64, creds: JWTCredentials) -> InstallationTokenGenerator {
412 InstallationTokenGenerator {
413 installation_id,
414 jwt_credential: Box::new(Credentials::JWT(creds)),
415 access_key: Arc::new(Mutex::new(None)),
416 }
417 }
418
419 fn token(&self) -> Option<String> {
420 if let Credentials::JWT(ref creds) = *self.jwt_credential {
421 if creds.is_stale() {
422 return None;
423 }
424 }
425 self.access_key.lock().unwrap().clone()
426 }
427
428 fn jwt(&self) -> &Credentials {
429 &*self.jwt_credential
430 }
431}
432
433impl PartialEq for InstallationTokenGenerator {
434 fn eq(&self, other: &InstallationTokenGenerator) -> bool {
435 self.installation_id == other.installation_id && self.jwt_credential == other.jwt_credential
436 }
437}
438
439#[derive(Clone, Debug)]
441pub struct Github {
442 host: String,
443 agent: String,
444 client: Client,
445 credentials: Option<Credentials>,
446 #[cfg(feature = "httpcache")]
447 http_cache: BoxedHttpCache,
448}
449
450impl Github {
451 pub fn new<A, C>(agent: A, credentials: C) -> Result<Self>
452 where
453 A: Into<String>,
454 C: Into<Option<Credentials>>,
455 {
456 Self::host(DEFAULT_HOST, agent, credentials)
457 }
458
459 pub fn host<H, A, C>(host: H, agent: A, credentials: C) -> Result<Self>
460 where
461 H: Into<String>,
462 A: Into<String>,
463 C: Into<Option<Credentials>>,
464 {
465 let http = Client::builder().build()?;
466 #[cfg(feature = "httpcache")]
467 {
468 Ok(Self::custom(
469 host,
470 agent,
471 credentials,
472 http,
473 HttpCache::noop(),
474 ))
475 }
476 #[cfg(not(feature = "httpcache"))]
477 {
478 Ok(Self::custom(host, agent, credentials, http))
479 }
480 }
481
482 #[cfg(feature = "httpcache")]
483 pub fn custom<H, A, CR>(
484 host: H,
485 agent: A,
486 credentials: CR,
487 http: Client,
488 http_cache: BoxedHttpCache,
489 ) -> Self
490 where
491 H: Into<String>,
492 A: Into<String>,
493 CR: Into<Option<Credentials>>,
494 {
495 Self {
496 host: host.into(),
497 agent: agent.into(),
498 client: http,
499 credentials: credentials.into(),
500 http_cache,
501 }
502 }
503
504 #[cfg(not(feature = "httpcache"))]
505 pub fn custom<H, A, CR>(host: H, agent: A, credentials: CR, http: Client) -> Self
506 where
507 H: Into<String>,
508 A: Into<String>,
509 CR: Into<Option<Credentials>>,
510 {
511 Self {
512 host: host.into(),
513 agent: agent.into(),
514 client: http,
515 credentials: credentials.into(),
516 }
517 }
518
519 pub fn set_credentials<CR>(&mut self, credentials: CR)
520 where
521 CR: Into<Option<Credentials>>,
522 {
523 self.credentials = credentials.into();
524 }
525
526 pub fn rate_limit(&self) -> RateLimit {
527 RateLimit::new(self.clone())
528 }
529
530 pub fn activity(&self) -> Activity {
532 Activity::new(self.clone())
533 }
534
535 pub fn repo<O, R>(&self, owner: O, repo: R) -> Repository
537 where
538 O: Into<String>,
539 R: Into<String>,
540 {
541 Repository::new(self.clone(), owner, repo)
542 }
543
544 pub fn user_repos<S>(&self, owner: S) -> UserRepositories
547 where
548 S: Into<String>,
549 {
550 UserRepositories::new(self.clone(), owner)
551 }
552
553 pub fn repos(&self) -> Repositories {
556 Repositories::new(self.clone())
557 }
558
559 pub fn org<O>(&self, org: O) -> Organization
560 where
561 O: Into<String>,
562 {
563 Organization::new(self.clone(), org)
564 }
565
566 pub fn orgs(&self) -> Organizations {
569 Organizations::new(self.clone())
570 }
571
572 pub fn users(&self) -> Users {
575 Users::new(self.clone())
576 }
577
578 pub fn user_orgs<U>(&self, user: U) -> UserOrganizations
581 where
582 U: Into<String>,
583 {
584 UserOrganizations::new(self.clone(), user)
585 }
586
587 pub fn user_gists<O>(&self, owner: O) -> UserGists
589 where
590 O: Into<String>,
591 {
592 UserGists::new(self.clone(), owner)
593 }
594
595 pub fn gists(&self) -> Gists {
598 Gists::new(self.clone())
599 }
600
601 pub fn search(&self) -> Search {
603 Search::new(self.clone())
604 }
605
606 pub fn org_repos<O>(&self, org: O) -> OrganizationRepositories
609 where
610 O: Into<String>,
611 {
612 OrganizationRepositories::new(self.clone(), org)
613 }
614
615 pub fn app(&self) -> App {
617 App::new(self.clone())
618 }
619
620 fn credentials(&self, authentication: AuthenticationConstraint) -> Option<&Credentials> {
621 match (authentication, self.credentials.as_ref()) {
622 (AuthenticationConstraint::Unconstrained, creds) => creds,
623 (AuthenticationConstraint::JWT, creds @ Some(&Credentials::JWT(_))) => creds,
624 (
625 AuthenticationConstraint::JWT,
626 Some(&Credentials::InstallationToken(ref apptoken)),
627 ) => Some(apptoken.jwt()),
628 (AuthenticationConstraint::JWT, creds) => {
629 error!(
630 "Request needs JWT authentication but only {:?} available",
631 creds
632 );
633 None
634 }
635 }
636 }
637
638 fn url_and_auth(
639 &self,
640 uri: &str,
641 authentication: AuthenticationConstraint,
642 ) -> Future<(Url, Option<String>)> {
643 let parsed_url = uri.parse::<Url>();
644
645 match self.credentials(authentication) {
646 Some(&Credentials::Client(ref id, ref secret)) => Box::pin(future::ready(
647 parsed_url
648 .map(|mut u| {
649 u.query_pairs_mut()
650 .append_pair("client_id", id)
651 .append_pair("client_secret", secret);
652 (u, None)
653 })
654 .map_err(Error::from),
655 )),
656 Some(&Credentials::Token(ref token)) => {
657 let auth = format!("token {}", token);
658 Box::pin(future::ready(
659 parsed_url.map(|u| (u, Some(auth))).map_err(Error::from),
660 ))
661 }
662 Some(&Credentials::JWT(ref jwt)) => {
663 let auth = format!("Bearer {}", jwt.token());
664 Box::pin(future::ready(
665 parsed_url.map(|u| (u, Some(auth))).map_err(Error::from),
666 ))
667 }
668 Some(&Credentials::InstallationToken(ref apptoken)) => {
669 if let Some(token) = apptoken.token() {
670 let auth = format!("token {}", token);
671 Box::pin(future::ready(
672 parsed_url.map(|u| (u, Some(auth))).map_err(Error::from),
673 ))
674 } else {
675 debug!("App token is stale, refreshing");
676 let token_ref = apptoken.access_key.clone();
677 Box::pin(
678 self.app()
679 .make_access_token(apptoken.installation_id)
680 .and_then(move |token| {
681 let auth = format!("token {}", &token.token);
682 *token_ref.lock().unwrap() = Some(token.token);
683 future::ready(
684 parsed_url.map(|u| (u, Some(auth))).map_err(Error::from),
685 )
686 }),
687 )
688 }
689 }
690 None => Box::pin(future::ready(
691 parsed_url.map(|u| (u, None)).map_err(Error::from),
692 )),
693 }
694 }
695
696 fn request<Out>(
697 &self,
698 method: Method,
699 uri: &str,
700 body: Option<Vec<u8>>,
701 media_type: MediaType,
702 authentication: AuthenticationConstraint,
703 ) -> Future<(Option<Link>, Out)>
704 where
705 Out: DeserializeOwned + 'static + Send,
706 {
707 let url_and_auth = self.url_and_auth(uri, authentication);
708
709 let instance = self.clone();
710 #[cfg(feature = "httpcache")]
711 let uri2 = uri.to_string();
712 let response = url_and_auth
713 .map_err(Error::from)
714 .and_then(move |(url, auth)| {
715 #[cfg(not(feature = "httpcache"))]
716 let mut req = instance.client.request(method, url);
717
718 #[cfg(feature = "httpcache")]
719 let mut req = {
720 let mut req = instance.client.request(method.clone(), url);
721 if method == Method::GET {
722 if let Ok(etag) = instance.http_cache.lookup_etag(&uri2) {
723 req = req.header(IF_NONE_MATCH, etag);
724 }
725 }
726 req
727 };
728
729 req = req.header(USER_AGENT, &*instance.agent);
730 req = req.header(
731 ACCEPT,
732 &*format!("{}", qitem::<Mime>(From::from(media_type))),
733 );
734
735 if let Some(auth_str) = auth {
736 req = req.header(AUTHORIZATION, &*auth_str);
737 }
738
739 trace!("Body: {:?}", &body);
740 if let Some(body) = body {
741 req = req.body(Body::from(body));
742 }
743 debug!("Request: {:?}", &req);
744 req.send().map_err(Error::from)
745 });
746
747 #[cfg(feature = "httpcache")]
748 let instance2 = self.clone();
749
750 #[cfg(feature = "httpcache")]
751 let uri3 = uri.to_string();
752 Box::pin(response.and_then(move |response| {
753 #[cfg(not(feature = "httpcache"))]
754 let (remaining, reset) = get_header_values(response.headers());
755 #[cfg(feature = "httpcache")]
756 let (remaining, reset, etag) = get_header_values(response.headers());
757
758 let status = response.status();
759 let link = response
760 .headers()
761 .get(LINK)
762 .and_then(|l| l.to_str().ok())
763 .and_then(|l| l.parse().ok());
764
765 Box::pin(
766 response
767 .bytes()
768 .map_err(Error::from)
769 .and_then(move |response_body| async move {
770 if status.is_success() {
771 debug!(
772 "response payload {}",
773 String::from_utf8_lossy(&response_body)
774 );
775 #[cfg(feature = "httpcache")]
776 {
777 if let Some(etag) = etag {
778 let next_link = link.as_ref().and_then(|l| next_link(&l));
779 if let Err(e) = instance2.http_cache.cache_response(
780 &uri3,
781 &response_body,
782 &etag,
783 &next_link,
784 ) {
785 debug!("Failed to cache body & etag: {}", e);
787 }
788 }
789 }
790 let parsed_response = if status == StatusCode::NO_CONTENT { serde_json::from_str("null") } else { serde_json::from_slice::<Out>(&response_body) };
791 parsed_response
792 .map(|out| (link, out))
793 .map_err(Error::Codec)
794 } else if status == StatusCode::NOT_MODIFIED {
795 #[cfg(feature = "httpcache")]
798 {
799 instance2
800 .http_cache
801 .lookup_body(&uri3)
802 .map_err(Error::from)
803 .and_then(|body| {
804 serde_json::from_str::<Out>(&body)
805 .map_err(Error::from)
806 .and_then(|out| {
807 let link = match link {
808 Some(link) => Ok(Some(link)),
809 None => instance2
810 .http_cache
811 .lookup_next_link(&uri3)
812 .map(|next_link| next_link.map(|next| {
813 let next = LinkValue::new(next).push_rel(RelationType::Next);
814 Link::new(vec![next])
815 }))
816 };
817 link.map(|link| (link, out))
818 })
819 })
820 }
821 #[cfg(not(feature = "httpcache"))]
822 {
823 unreachable!("this should not be reachable without the httpcache feature enabled")
824 }
825 } else {
826 let error = match (remaining, reset) {
827 (Some(remaining), Some(reset)) if remaining == 0 => {
828 let now = SystemTime::now()
829 .duration_since(UNIX_EPOCH)
830 .unwrap()
831 .as_secs();
832 Error::RateLimit {
833 reset: Duration::from_secs(u64::from(reset) - now),
834 }
835 }
836 _ => Error::Fault {
837 code: status,
838 error: serde_json::from_slice(&response_body)?,
839 },
840 };
841 Err(error)
842 }
843 }),
844 )
845 }))
846 }
847
848 fn request_entity<D>(
849 &self,
850 method: Method,
851 uri: &str,
852 body: Option<Vec<u8>>,
853 media_type: MediaType,
854 authentication: AuthenticationConstraint,
855 ) -> Future<D>
856 where
857 D: DeserializeOwned + 'static + Send,
858 {
859 Box::pin(
860 self.request(method, uri, body, media_type, authentication)
861 .map_ok(|(_, entity)| entity),
862 )
863 }
864
865 fn get<D>(&self, uri: &str) -> Future<D>
866 where
867 D: DeserializeOwned + 'static + Send,
868 {
869 self.get_media(uri, MediaType::Json)
870 }
871
872 fn get_media<D>(&self, uri: &str, media: MediaType) -> Future<D>
873 where
874 D: DeserializeOwned + 'static + Send,
875 {
876 self.request_entity(
877 Method::GET,
878 &(self.host.clone() + uri),
879 None,
880 media,
881 AuthenticationConstraint::Unconstrained,
882 )
883 }
884
885 fn get_stream<D>(&self, uri: &str) -> Stream<D>
886 where
887 D: DeserializeOwned + 'static + Send,
888 {
889 unfold(self.clone(), self.get_pages(uri), |x| x)
890 }
891
892 fn get_pages<D>(&self, uri: &str) -> Future<(Option<Link>, D)>
893 where
894 D: DeserializeOwned + 'static + Send,
895 {
896 self.request(
897 Method::GET,
898 &(self.host.clone() + uri),
899 None,
900 MediaType::Json,
901 AuthenticationConstraint::Unconstrained,
902 )
903 }
904
905 fn get_pages_url<D>(&self, url: &Url) -> Future<(Option<Link>, D)>
906 where
907 D: DeserializeOwned + 'static + Send,
908 {
909 self.request(
910 Method::GET,
911 url.as_str(),
912 None,
913 MediaType::Json,
914 AuthenticationConstraint::Unconstrained,
915 )
916 }
917
918 fn delete(&self, uri: &str) -> Future<()> {
919 Box::pin(
920 self.request_entity::<()>(
921 Method::DELETE,
922 &(self.host.clone() + uri),
923 None,
924 MediaType::Json,
925 AuthenticationConstraint::Unconstrained,
926 )
927 .or_else(|err| async move {
928 match err {
929 Error::Codec(_) => Ok(()),
930 otherwise => Err(otherwise),
931 }
932 }),
933 )
934 }
935
936 fn delete_message(&self, uri: &str, message: Vec<u8>) -> Future<()> {
937 Box::pin(
938 self.request_entity::<()>(
939 Method::DELETE,
940 &(self.host.clone() + uri),
941 Some(message),
942 MediaType::Json,
943 AuthenticationConstraint::Unconstrained,
944 )
945 .or_else(|err| async move {
946 match err {
947 Error::Codec(_) => Ok(()),
948 otherwise => Err(otherwise),
949 }
950 }),
951 )
952 }
953
954 fn post<D>(&self, uri: &str, message: Vec<u8>) -> Future<D>
955 where
956 D: DeserializeOwned + 'static + Send,
957 {
958 self.post_media(
959 uri,
960 message,
961 MediaType::Json,
962 AuthenticationConstraint::Unconstrained,
963 )
964 }
965
966 fn post_media<D>(
967 &self,
968 uri: &str,
969 message: Vec<u8>,
970 media: MediaType,
971 authentication: AuthenticationConstraint,
972 ) -> Future<D>
973 where
974 D: DeserializeOwned + 'static + Send,
975 {
976 self.request_entity(
977 Method::POST,
978 &(self.host.clone() + uri),
979 Some(message),
980 media,
981 authentication,
982 )
983 }
984
985 fn patch_no_response(&self, uri: &str, message: Vec<u8>) -> Future<()> {
986 Box::pin(self.patch(uri, message).or_else(|err| async move {
987 match err {
988 Error::Codec(_) => Ok(()),
989 err => Err(err),
990 }
991 }))
992 }
993
994 fn patch_media<D>(&self, uri: &str, message: Vec<u8>, media: MediaType) -> Future<D>
995 where
996 D: DeserializeOwned + 'static + Send,
997 {
998 self.request_entity(
999 Method::PATCH,
1000 &(self.host.clone() + uri),
1001 Some(message),
1002 media,
1003 AuthenticationConstraint::Unconstrained,
1004 )
1005 }
1006
1007 fn patch<D>(&self, uri: &str, message: Vec<u8>) -> Future<D>
1008 where
1009 D: DeserializeOwned + 'static + Send,
1010 {
1011 self.patch_media(uri, message, MediaType::Json)
1012 }
1013
1014 fn put_no_response(&self, uri: &str, message: Vec<u8>) -> Future<()> {
1015 Box::pin(self.put(uri, message).or_else(|err| async move {
1016 match err {
1017 Error::Codec(_) => Ok(()),
1018 err => Err(err),
1019 }
1020 }))
1021 }
1022
1023 fn put<D>(&self, uri: &str, message: Vec<u8>) -> Future<D>
1024 where
1025 D: DeserializeOwned + 'static + Send,
1026 {
1027 self.put_media(uri, message, MediaType::Json)
1028 }
1029
1030 fn put_media<D>(&self, uri: &str, message: Vec<u8>, media: MediaType) -> Future<D>
1031 where
1032 D: DeserializeOwned + 'static + Send,
1033 {
1034 self.request_entity(
1035 Method::PUT,
1036 &(self.host.clone() + uri),
1037 Some(message),
1038 media,
1039 AuthenticationConstraint::Unconstrained,
1040 )
1041 }
1042}
1043
1044#[cfg(not(feature = "httpcache"))]
1045type HeaderValues = (Option<u32>, Option<u32>);
1046#[cfg(feature = "httpcache")]
1047type HeaderValues = (Option<u32>, Option<u32>, Option<Vec<u8>>);
1048
1049fn get_header_values(headers: &HeaderMap<HeaderValue>) -> HeaderValues {
1050 if let Some(value) = headers.get(X_GITHUB_REQUEST_ID) {
1051 debug!("x-github-request-id: {:?}", value)
1052 }
1053 if let Some(value) = headers.get(X_RATELIMIT_LIMIT) {
1054 debug!("x-rate-limit-limit: {:?}", value)
1055 }
1056 let remaining = headers
1057 .get(X_RATELIMIT_REMAINING)
1058 .and_then(|val| val.to_str().ok())
1059 .and_then(|val| val.parse::<u32>().ok());
1060 let reset = headers
1061 .get(X_RATELIMIT_RESET)
1062 .and_then(|val| val.to_str().ok())
1063 .and_then(|val| val.parse::<u32>().ok());
1064 if let Some(value) = remaining {
1065 debug!("x-rate-limit-remaining: {}", value)
1066 }
1067 if let Some(value) = reset {
1068 debug!("x-rate-limit-reset: {}", value)
1069 }
1070 let etag = headers.get(ETAG);
1071 if let Some(value) = etag {
1072 debug!("etag: {:?}", value)
1073 }
1074
1075 #[cfg(feature = "httpcache")]
1076 {
1077 let etag = etag.map(|etag| etag.as_bytes().to_vec());
1078 (remaining, reset, etag)
1079 }
1080 #[cfg(not(feature = "httpcache"))]
1081 (remaining, reset)
1082}
1083
1084fn next_link(l: &Link) -> Option<String> {
1085 l.values().iter().find_map(|value| {
1086 value.rel().and_then(|rels| {
1087 if rels.iter().any(|rel| rel == &RelationType::Next) {
1088 Some(value.link().into())
1089 } else {
1090 None
1091 }
1092 })
1093 })
1094}
1095
1096fn unfold<D, I>(
1098 github: Github,
1099 first: Future<(Option<Link>, D)>,
1100 into_items: fn(D) -> Vec<I>,
1101) -> Stream<I>
1102where
1103 D: DeserializeOwned + 'static + Send,
1104 I: 'static + Send,
1105{
1106 Box::pin(
1107 first
1108 .map_ok(move |(link, payload)| {
1109 let mut items = into_items(payload);
1110 items.reverse();
1111 stream::try_unfold(
1112 (github, link, items),
1113 move |(github, link, mut items)| async move {
1114 match items.pop() {
1115 Some(item) => Ok(Some((item, (github, link, items)))),
1116 None => match link.and_then(|l| next_link(&l)) {
1117 Some(url) => {
1118 let url = Url::from_str(&url).unwrap();
1119 let (link, payload) = github.get_pages_url(&url).await?;
1120 let mut items = into_items(payload);
1121 let item = items.remove(0);
1122 items.reverse();
1123 Ok(Some((item, (github, link, items))))
1124 }
1125 None => Ok(None),
1126 },
1127 }
1128 },
1129 )
1130 })
1131 .try_flatten_stream(),
1132 )
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::*;
1138
1139 #[test]
1140 fn credentials_impl_debug() {
1141 assert_eq!(
1142 format!("{:?}", Credentials::Token("secret".into())),
1143 "Credentials::Token(\"******\")"
1144 );
1145 assert_eq!(
1146 format!(
1147 "{:?}",
1148 Credentials::Client("client_id".into(), "client_secret".into())
1149 ),
1150 "Credentials::Client(\"client_id\", \"*************\")"
1151 );
1152 }
1153
1154 #[test]
1155 fn default_sort_direction() {
1156 let default: SortDirection = Default::default();
1157 assert_eq!(default, SortDirection::Asc)
1158 }
1159
1160 #[test]
1161 #[cfg(not(feature = "httpcache"))]
1162 fn header_values() {
1163 let empty = HeaderMap::new();
1164 let actual = get_header_values(&empty);
1165 let expected = (None, None);
1166 assert_eq!(actual, expected);
1167
1168 let mut all_valid = HeaderMap::new();
1169 all_valid.insert(X_RATELIMIT_REMAINING, HeaderValue::from_static("1234"));
1170 all_valid.insert(X_RATELIMIT_RESET, HeaderValue::from_static("5678"));
1171 let actual = get_header_values(&all_valid);
1172 let expected = (Some(1234), Some(5678));
1173 assert_eq!(actual, expected);
1174
1175 let mut invalid = HeaderMap::new();
1176 invalid.insert(X_RATELIMIT_REMAINING, HeaderValue::from_static("foo"));
1177 invalid.insert(X_RATELIMIT_RESET, HeaderValue::from_static("bar"));
1178 let actual = get_header_values(&invalid);
1179 let expected = (None, None);
1180 assert_eq!(actual, expected);
1181 }
1182
1183 #[test]
1184 #[cfg(feature = "httpcache")]
1185 fn header_values() {
1186 let empty = HeaderMap::new();
1187 let actual = get_header_values(&empty);
1188 let expected = (None, None, None);
1189 assert_eq!(actual, expected);
1190
1191 let mut all_valid = HeaderMap::new();
1192 all_valid.insert(X_RATELIMIT_REMAINING, HeaderValue::from_static("1234"));
1193 all_valid.insert(X_RATELIMIT_RESET, HeaderValue::from_static("5678"));
1194 all_valid.insert(ETAG, HeaderValue::from_static("foobar"));
1195 let actual = get_header_values(&all_valid);
1196 let expected = (Some(1234), Some(5678), Some(b"foobar".to_vec()));
1197 assert_eq!(actual, expected);
1198
1199 let mut invalid = HeaderMap::new();
1200 invalid.insert(X_RATELIMIT_REMAINING, HeaderValue::from_static("foo"));
1201 invalid.insert(X_RATELIMIT_RESET, HeaderValue::from_static("bar"));
1202 invalid.insert(ETAG, HeaderValue::from_static(""));
1203 let actual = get_header_values(&invalid);
1204 let expected = (None, None, Some(Vec::new()));
1205 assert_eq!(actual, expected);
1206 }
1207}