#![allow(missing_docs)]
use std::fmt;
use std::pin::Pin;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use futures::{future, prelude::*, stream, Future as StdFuture, Stream as StdStream};
#[cfg(feature = "httpcache")]
use http::header::IF_NONE_MATCH;
use http::header::{HeaderMap, HeaderValue};
use http::header::{ACCEPT, AUTHORIZATION, ETAG, LINK, USER_AGENT};
use http::{Method, StatusCode};
#[cfg(feature = "httpcache")]
use hyperx::header::LinkValue;
use hyperx::header::{qitem, Link, RelationType};
use jsonwebtoken as jwt;
use log::{debug, error, trace};
use mime::Mime;
use reqwest::Url;
use reqwest::{Body, Client};
use serde::de::DeserializeOwned;
use serde::Serialize;
#[doc(hidden)]
#[cfg(feature = "httpcache")]
pub mod http_cache;
#[macro_use]
mod macros;
pub mod activity;
pub mod app;
pub mod branches;
pub mod checks;
pub mod collaborators;
pub mod comments;
pub mod content;
pub mod deployments;
pub mod errors;
pub mod gists;
pub mod git;
pub mod hooks;
pub mod issues;
pub mod keys;
pub mod labels;
pub mod membership;
pub mod notifications;
pub mod organizations;
pub mod pull_commits;
pub mod pulls;
pub mod rate_limit;
pub mod releases;
pub mod repo_commits;
pub mod repositories;
pub mod review_comments;
pub mod review_requests;
pub mod search;
pub mod stars;
pub mod statuses;
pub mod teams;
pub mod traffic;
pub mod users;
pub mod watching;
pub mod milestone;
pub use crate::errors::{Error, Result};
#[cfg(feature = "httpcache")]
pub use crate::http_cache::{BoxedHttpCache, HttpCache};
use crate::activity::Activity;
use crate::app::App;
use crate::gists::{Gists, UserGists};
use crate::organizations::{Organization, Organizations, UserOrganizations};
use crate::rate_limit::RateLimit;
use crate::repositories::{OrganizationRepositories, Repositories, Repository, UserRepositories};
use crate::search::Search;
use crate::users::Users;
const DEFAULT_HOST: &str = "https://api.github.com";
const MAX_JWT_TOKEN_LIFE: time::Duration = time::Duration::from_secs(60 * 9);
const JWT_TOKEN_REFRESH_PERIOD: time::Duration = time::Duration::from_secs(60 * 8);
pub type Future<T> = Pin<Box<dyn StdFuture<Output = Result<T>> + Send>>;
pub type Stream<T> = Pin<Box<dyn StdStream<Item = Result<T>> + Send>>;
const X_GITHUB_REQUEST_ID: &str = "x-github-request-id";
const X_RATELIMIT_LIMIT: &str = "x-ratelimit-limit";
const X_RATELIMIT_REMAINING: &str = "x-ratelimit-remaining";
const X_RATELIMIT_RESET: &str = "x-ratelimit-reset";
pub(crate) mod utils {
pub use percent_encoding::percent_encode;
use percent_encoding::{AsciiSet, CONTROLS};
const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
pub const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
pub const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'%');
}
#[derive(Clone, Copy)]
pub enum MediaType {
Json,
Preview(&'static str),
}
impl Default for MediaType {
fn default() -> MediaType {
MediaType::Json
}
}
impl From<MediaType> for Mime {
fn from(media: MediaType) -> Mime {
match media {
MediaType::Json => "application/vnd.github.v3+json".parse().unwrap(),
MediaType::Preview(codename) => {
format!("application/vnd.github.{}-preview+json", codename)
.parse()
.unwrap_or_else(|_| {
panic!("could not parse media type for preview {}", codename)
})
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AuthenticationConstraint {
Unconstrained,
JWT,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SortDirection {
Asc,
Desc,
}
impl fmt::Display for SortDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
SortDirection::Asc => "asc",
SortDirection::Desc => "desc",
}
.fmt(f)
}
}
impl Default for SortDirection {
fn default() -> SortDirection {
SortDirection::Asc
}
}
#[derive(PartialEq, Clone)]
pub enum Credentials {
Token(String),
Client(String, String),
JWT(JWTCredentials),
InstallationToken(InstallationTokenGenerator),
}
impl fmt::Debug for Credentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Credentials::Token(value) => f
.debug_tuple("Credentials::Token")
.field(&"*".repeat(value.len()))
.finish(),
Credentials::Client(id, secret) => f
.debug_tuple("Credentials::Client")
.field(&id)
.field(&"*".repeat(secret.len()))
.finish(),
Credentials::JWT(jwt) => f
.debug_struct("Credentials::JWT")
.field("app_id", &jwt.app_id)
.field("private_key", &"vec![***]")
.finish(),
Credentials::InstallationToken(generator) => f
.debug_struct("Credentials::InstallationToken")
.field("installation_id", &generator.installation_id)
.field("jwt_credential", &"***")
.finish(),
}
}
}
#[derive(Clone)]
pub struct JWTCredentials {
pub app_id: u64,
pub private_key: Vec<u8>,
cache: Arc<Mutex<ExpiringJWTCredential>>,
}
impl JWTCredentials {
pub fn new(app_id: u64, private_key: Vec<u8>) -> Result<JWTCredentials> {
let creds = ExpiringJWTCredential::calculate(app_id, &private_key)?;
Ok(JWTCredentials {
app_id,
private_key,
cache: Arc::new(Mutex::new(creds)),
})
}
fn is_stale(&self) -> bool {
self.cache.lock().unwrap().is_stale()
}
pub fn token(&self) -> String {
let mut expiring = self.cache.lock().unwrap();
if expiring.is_stale() {
*expiring = ExpiringJWTCredential::calculate(self.app_id, &self.private_key)
.expect("JWT private key worked before, it should work now...");
}
expiring.token.clone()
}
}
impl PartialEq for JWTCredentials {
fn eq(&self, other: &JWTCredentials) -> bool {
self.app_id == other.app_id && self.private_key == other.private_key
}
}
#[derive(Debug)]
struct ExpiringJWTCredential {
token: String,
created_at: time::Instant,
}
#[derive(Serialize)]
struct JWTCredentialClaim {
iat: u64,
exp: u64,
iss: u64,
}
impl ExpiringJWTCredential {
fn calculate(app_id: u64, private_key: &[u8]) -> Result<ExpiringJWTCredential> {
let created_at = time::Instant::now();
let now = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.unwrap();
let expires = now + MAX_JWT_TOKEN_LIFE;
let payload = JWTCredentialClaim {
iat: now.as_secs(),
exp: expires.as_secs(),
iss: app_id,
};
let header = jwt::Header::new(jwt::Algorithm::RS256);
let jwt = jwt::encode(
&header,
&payload,
&jsonwebtoken::EncodingKey::from_rsa_der(private_key),
)?;
Ok(ExpiringJWTCredential {
created_at,
token: jwt,
})
}
fn is_stale(&self) -> bool {
self.created_at.elapsed() >= JWT_TOKEN_REFRESH_PERIOD
}
}
#[derive(Debug, Clone)]
pub struct InstallationTokenGenerator {
pub installation_id: u64,
pub jwt_credential: Box<Credentials>,
access_key: Arc<Mutex<Option<String>>>,
}
impl InstallationTokenGenerator {
pub fn new(installation_id: u64, creds: JWTCredentials) -> InstallationTokenGenerator {
InstallationTokenGenerator {
installation_id,
jwt_credential: Box::new(Credentials::JWT(creds)),
access_key: Arc::new(Mutex::new(None)),
}
}
fn token(&self) -> Option<String> {
if let Credentials::JWT(ref creds) = *self.jwt_credential {
if creds.is_stale() {
return None;
}
}
self.access_key.lock().unwrap().clone()
}
fn jwt(&self) -> &Credentials {
&*self.jwt_credential
}
}
impl PartialEq for InstallationTokenGenerator {
fn eq(&self, other: &InstallationTokenGenerator) -> bool {
self.installation_id == other.installation_id && self.jwt_credential == other.jwt_credential
}
}
#[derive(Clone, Debug)]
pub struct Github {
host: String,
agent: String,
client: Client,
credentials: Option<Credentials>,
#[cfg(feature = "httpcache")]
http_cache: BoxedHttpCache,
}
impl Github {
pub fn new<A, C>(agent: A, credentials: C) -> Result<Self>
where
A: Into<String>,
C: Into<Option<Credentials>>,
{
Self::host(DEFAULT_HOST, agent, credentials)
}
pub fn host<H, A, C>(host: H, agent: A, credentials: C) -> Result<Self>
where
H: Into<String>,
A: Into<String>,
C: Into<Option<Credentials>>,
{
let http = Client::builder().build()?;
#[cfg(feature = "httpcache")]
{
Ok(Self::custom(
host,
agent,
credentials,
http,
HttpCache::noop(),
))
}
#[cfg(not(feature = "httpcache"))]
{
Ok(Self::custom(host, agent, credentials, http))
}
}
#[cfg(feature = "httpcache")]
pub fn custom<H, A, CR>(
host: H,
agent: A,
credentials: CR,
http: Client,
http_cache: BoxedHttpCache,
) -> Self
where
H: Into<String>,
A: Into<String>,
CR: Into<Option<Credentials>>,
{
Self {
host: host.into(),
agent: agent.into(),
client: http,
credentials: credentials.into(),
http_cache,
}
}
#[cfg(not(feature = "httpcache"))]
pub fn custom<H, A, CR>(host: H, agent: A, credentials: CR, http: Client) -> Self
where
H: Into<String>,
A: Into<String>,
CR: Into<Option<Credentials>>,
{
Self {
host: host.into(),
agent: agent.into(),
client: http,
credentials: credentials.into(),
}
}
pub fn set_credentials<CR>(&mut self, credentials: CR)
where
CR: Into<Option<Credentials>>,
{
self.credentials = credentials.into();
}
pub fn rate_limit(&self) -> RateLimit {
RateLimit::new(self.clone())
}
pub fn activity(&self) -> Activity {
Activity::new(self.clone())
}
pub fn repo<O, R>(&self, owner: O, repo: R) -> Repository
where
O: Into<String>,
R: Into<String>,
{
Repository::new(self.clone(), owner, repo)
}
pub fn user_repos<S>(&self, owner: S) -> UserRepositories
where
S: Into<String>,
{
UserRepositories::new(self.clone(), owner)
}
pub fn repos(&self) -> Repositories {
Repositories::new(self.clone())
}
pub fn org<O>(&self, org: O) -> Organization
where
O: Into<String>,
{
Organization::new(self.clone(), org)
}
pub fn orgs(&self) -> Organizations {
Organizations::new(self.clone())
}
pub fn users(&self) -> Users {
Users::new(self.clone())
}
pub fn user_orgs<U>(&self, user: U) -> UserOrganizations
where
U: Into<String>,
{
UserOrganizations::new(self.clone(), user)
}
pub fn user_gists<O>(&self, owner: O) -> UserGists
where
O: Into<String>,
{
UserGists::new(self.clone(), owner)
}
pub fn gists(&self) -> Gists {
Gists::new(self.clone())
}
pub fn search(&self) -> Search {
Search::new(self.clone())
}
pub fn org_repos<O>(&self, org: O) -> OrganizationRepositories
where
O: Into<String>,
{
OrganizationRepositories::new(self.clone(), org)
}
pub fn app(&self) -> App {
App::new(self.clone())
}
fn credentials(&self, authentication: AuthenticationConstraint) -> Option<&Credentials> {
match (authentication, self.credentials.as_ref()) {
(AuthenticationConstraint::Unconstrained, creds) => creds,
(AuthenticationConstraint::JWT, creds @ Some(&Credentials::JWT(_))) => creds,
(
AuthenticationConstraint::JWT,
Some(&Credentials::InstallationToken(ref apptoken)),
) => Some(apptoken.jwt()),
(AuthenticationConstraint::JWT, creds) => {
error!(
"Request needs JWT authentication but only {:?} available",
creds
);
None
}
}
}
fn url_and_auth(
&self,
uri: &str,
authentication: AuthenticationConstraint,
) -> Future<(Url, Option<String>)> {
let parsed_url = uri.parse::<Url>();
match self.credentials(authentication) {
Some(&Credentials::Client(ref id, ref secret)) => Box::pin(future::ready(
parsed_url
.map(|mut u| {
u.query_pairs_mut()
.append_pair("client_id", id)
.append_pair("client_secret", secret);
(u, None)
})
.map_err(Error::from),
)),
Some(&Credentials::Token(ref token)) => {
let auth = format!("token {}", token);
Box::pin(future::ready(
parsed_url.map(|u| (u, Some(auth))).map_err(Error::from),
))
}
Some(&Credentials::JWT(ref jwt)) => {
let auth = format!("Bearer {}", jwt.token());
Box::pin(future::ready(
parsed_url.map(|u| (u, Some(auth))).map_err(Error::from),
))
}
Some(&Credentials::InstallationToken(ref apptoken)) => {
if let Some(token) = apptoken.token() {
let auth = format!("token {}", token);
Box::pin(future::ready(
parsed_url.map(|u| (u, Some(auth))).map_err(Error::from),
))
} else {
debug!("App token is stale, refreshing");
let token_ref = apptoken.access_key.clone();
Box::pin(
self.app()
.make_access_token(apptoken.installation_id)
.and_then(move |token| {
let auth = format!("token {}", &token.token);
*token_ref.lock().unwrap() = Some(token.token);
future::ready(
parsed_url.map(|u| (u, Some(auth))).map_err(Error::from),
)
}),
)
}
}
None => Box::pin(future::ready(
parsed_url.map(|u| (u, None)).map_err(Error::from),
)),
}
}
fn request<Out>(
&self,
method: Method,
uri: &str,
body: Option<Vec<u8>>,
media_type: MediaType,
authentication: AuthenticationConstraint,
) -> Future<(Option<Link>, Out)>
where
Out: DeserializeOwned + 'static + Send,
{
let url_and_auth = self.url_and_auth(uri, authentication);
let instance = self.clone();
#[cfg(feature = "httpcache")]
let uri2 = uri.to_string();
let response = url_and_auth
.map_err(Error::from)
.and_then(move |(url, auth)| {
#[cfg(not(feature = "httpcache"))]
let mut req = instance.client.request(method, url);
#[cfg(feature = "httpcache")]
let mut req = {
let mut req = instance.client.request(method.clone(), url);
if method == Method::GET {
if let Ok(etag) = instance.http_cache.lookup_etag(&uri2) {
req = req.header(IF_NONE_MATCH, etag);
}
}
req
};
req = req.header(USER_AGENT, &*instance.agent);
req = req.header(
ACCEPT,
&*format!("{}", qitem::<Mime>(From::from(media_type))),
);
if let Some(auth_str) = auth {
req = req.header(AUTHORIZATION, &*auth_str);
}
trace!("Body: {:?}", &body);
if let Some(body) = body {
req = req.body(Body::from(body));
}
debug!("Request: {:?}", &req);
req.send().map_err(Error::from)
});
#[cfg(feature = "httpcache")]
let instance2 = self.clone();
#[cfg(feature = "httpcache")]
let uri3 = uri.to_string();
Box::pin(response.and_then(move |response| {
#[cfg(not(feature = "httpcache"))]
let (remaining, reset) = get_header_values(response.headers());
#[cfg(feature = "httpcache")]
let (remaining, reset, etag) = get_header_values(response.headers());
let status = response.status();
let link = response
.headers()
.get(LINK)
.and_then(|l| l.to_str().ok())
.and_then(|l| l.parse().ok());
Box::pin(
response
.bytes()
.map_err(Error::from)
.and_then(move |response_body| async move {
if status.is_success() {
debug!(
"response payload {}",
String::from_utf8_lossy(&response_body)
);
#[cfg(feature = "httpcache")]
{
if let Some(etag) = etag {
let next_link = link.as_ref().and_then(|l| next_link(&l));
if let Err(e) = instance2.http_cache.cache_response(
&uri3,
&response_body,
&etag,
&next_link,
) {
debug!("Failed to cache body & etag: {}", e);
}
}
}
let parsed_response = if status == StatusCode::NO_CONTENT { serde_json::from_str("null") } else { serde_json::from_slice::<Out>(&response_body) };
parsed_response
.map(|out| (link, out))
.map_err(Error::Codec)
} else if status == StatusCode::NOT_MODIFIED {
#[cfg(feature = "httpcache")]
{
instance2
.http_cache
.lookup_body(&uri3)
.map_err(Error::from)
.and_then(|body| {
serde_json::from_str::<Out>(&body)
.map_err(Error::from)
.and_then(|out| {
let link = match link {
Some(link) => Ok(Some(link)),
None => instance2
.http_cache
.lookup_next_link(&uri3)
.map(|next_link| next_link.map(|next| {
let next = LinkValue::new(next).push_rel(RelationType::Next);
Link::new(vec![next])
}))
};
link.map(|link| (link, out))
})
})
}
#[cfg(not(feature = "httpcache"))]
{
unreachable!("this should not be reachable without the httpcache feature enabled")
}
} else {
let error = match (remaining, reset) {
(Some(remaining), Some(reset)) if remaining == 0 => {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
Error::RateLimit {
reset: Duration::from_secs(u64::from(reset) - now),
}
}
_ => Error::Fault {
code: status,
error: serde_json::from_slice(&response_body)?,
},
};
Err(error)
}
}),
)
}))
}
fn request_entity<D>(
&self,
method: Method,
uri: &str,
body: Option<Vec<u8>>,
media_type: MediaType,
authentication: AuthenticationConstraint,
) -> Future<D>
where
D: DeserializeOwned + 'static + Send,
{
Box::pin(
self.request(method, uri, body, media_type, authentication)
.map_ok(|(_, entity)| entity),
)
}
fn get<D>(&self, uri: &str) -> Future<D>
where
D: DeserializeOwned + 'static + Send,
{
self.get_media(uri, MediaType::Json)
}
fn get_media<D>(&self, uri: &str, media: MediaType) -> Future<D>
where
D: DeserializeOwned + 'static + Send,
{
self.request_entity(
Method::GET,
&(self.host.clone() + uri),
None,
media,
AuthenticationConstraint::Unconstrained,
)
}
fn get_stream<D>(&self, uri: &str) -> Stream<D>
where
D: DeserializeOwned + 'static + Send,
{
unfold(self.clone(), self.get_pages(uri), |x| x)
}
fn get_pages<D>(&self, uri: &str) -> Future<(Option<Link>, D)>
where
D: DeserializeOwned + 'static + Send,
{
self.request(
Method::GET,
&(self.host.clone() + uri),
None,
MediaType::Json,
AuthenticationConstraint::Unconstrained,
)
}
fn get_pages_url<D>(&self, url: &Url) -> Future<(Option<Link>, D)>
where
D: DeserializeOwned + 'static + Send,
{
self.request(
Method::GET,
url.as_str(),
None,
MediaType::Json,
AuthenticationConstraint::Unconstrained,
)
}
fn delete(&self, uri: &str) -> Future<()> {
Box::pin(
self.request_entity::<()>(
Method::DELETE,
&(self.host.clone() + uri),
None,
MediaType::Json,
AuthenticationConstraint::Unconstrained,
)
.or_else(|err| async move {
match err {
Error::Codec(_) => Ok(()),
otherwise => Err(otherwise),
}
}),
)
}
fn delete_message(&self, uri: &str, message: Vec<u8>) -> Future<()> {
Box::pin(
self.request_entity::<()>(
Method::DELETE,
&(self.host.clone() + uri),
Some(message),
MediaType::Json,
AuthenticationConstraint::Unconstrained,
)
.or_else(|err| async move {
match err {
Error::Codec(_) => Ok(()),
otherwise => Err(otherwise),
}
}),
)
}
fn post<D>(&self, uri: &str, message: Vec<u8>) -> Future<D>
where
D: DeserializeOwned + 'static + Send,
{
self.post_media(
uri,
message,
MediaType::Json,
AuthenticationConstraint::Unconstrained,
)
}
fn post_media<D>(
&self,
uri: &str,
message: Vec<u8>,
media: MediaType,
authentication: AuthenticationConstraint,
) -> Future<D>
where
D: DeserializeOwned + 'static + Send,
{
self.request_entity(
Method::POST,
&(self.host.clone() + uri),
Some(message),
media,
authentication,
)
}
fn patch_no_response(&self, uri: &str, message: Vec<u8>) -> Future<()> {
Box::pin(self.patch(uri, message).or_else(|err| async move {
match err {
Error::Codec(_) => Ok(()),
err => Err(err),
}
}))
}
fn patch_media<D>(&self, uri: &str, message: Vec<u8>, media: MediaType) -> Future<D>
where
D: DeserializeOwned + 'static + Send,
{
self.request_entity(
Method::PATCH,
&(self.host.clone() + uri),
Some(message),
media,
AuthenticationConstraint::Unconstrained,
)
}
fn patch<D>(&self, uri: &str, message: Vec<u8>) -> Future<D>
where
D: DeserializeOwned + 'static + Send,
{
self.patch_media(uri, message, MediaType::Json)
}
fn put_no_response(&self, uri: &str, message: Vec<u8>) -> Future<()> {
Box::pin(self.put(uri, message).or_else(|err| async move {
match err {
Error::Codec(_) => Ok(()),
err => Err(err),
}
}))
}
fn put<D>(&self, uri: &str, message: Vec<u8>) -> Future<D>
where
D: DeserializeOwned + 'static + Send,
{
self.put_media(uri, message, MediaType::Json)
}
fn put_media<D>(&self, uri: &str, message: Vec<u8>, media: MediaType) -> Future<D>
where
D: DeserializeOwned + 'static + Send,
{
self.request_entity(
Method::PUT,
&(self.host.clone() + uri),
Some(message),
media,
AuthenticationConstraint::Unconstrained,
)
}
}
#[cfg(not(feature = "httpcache"))]
type HeaderValues = (Option<u32>, Option<u32>);
#[cfg(feature = "httpcache")]
type HeaderValues = (Option<u32>, Option<u32>, Option<Vec<u8>>);
fn get_header_values(headers: &HeaderMap<HeaderValue>) -> HeaderValues {
if let Some(value) = headers.get(X_GITHUB_REQUEST_ID) {
debug!("x-github-request-id: {:?}", value)
}
if let Some(value) = headers.get(X_RATELIMIT_LIMIT) {
debug!("x-rate-limit-limit: {:?}", value)
}
let remaining = headers
.get(X_RATELIMIT_REMAINING)
.and_then(|val| val.to_str().ok())
.and_then(|val| val.parse::<u32>().ok());
let reset = headers
.get(X_RATELIMIT_RESET)
.and_then(|val| val.to_str().ok())
.and_then(|val| val.parse::<u32>().ok());
if let Some(value) = remaining {
debug!("x-rate-limit-remaining: {}", value)
}
if let Some(value) = reset {
debug!("x-rate-limit-reset: {}", value)
}
let etag = headers.get(ETAG);
if let Some(value) = etag {
debug!("etag: {:?}", value)
}
#[cfg(feature = "httpcache")]
{
let etag = etag.map(|etag| etag.as_bytes().to_vec());
(remaining, reset, etag)
}
#[cfg(not(feature = "httpcache"))]
(remaining, reset)
}
fn next_link(l: &Link) -> Option<String> {
l.values().iter().find_map(|value| {
value.rel().and_then(|rels| {
if rels.iter().any(|rel| rel == &RelationType::Next) {
Some(value.link().into())
} else {
None
}
})
})
}
fn unfold<D, I>(
github: Github,
first: Future<(Option<Link>, D)>,
into_items: fn(D) -> Vec<I>,
) -> Stream<I>
where
D: DeserializeOwned + 'static + Send,
I: 'static + Send,
{
Box::pin(
first
.map_ok(move |(link, payload)| {
let mut items = into_items(payload);
items.reverse();
stream::try_unfold(
(github, link, items),
move |(github, link, mut items)| async move {
match items.pop() {
Some(item) => Ok(Some((item, (github, link, items)))),
None => match link.and_then(|l| next_link(&l)) {
Some(url) => {
let url = Url::from_str(&url).unwrap();
let (link, payload) = github.get_pages_url(&url).await?;
let mut items = into_items(payload);
let item = items.remove(0);
items.reverse();
Ok(Some((item, (github, link, items))))
}
None => Ok(None),
},
}
},
)
})
.try_flatten_stream(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn credentials_impl_debug() {
assert_eq!(
format!("{:?}", Credentials::Token("secret".into())),
"Credentials::Token(\"******\")"
);
assert_eq!(
format!(
"{:?}",
Credentials::Client("client_id".into(), "client_secret".into())
),
"Credentials::Client(\"client_id\", \"*************\")"
);
}
#[test]
fn default_sort_direction() {
let default: SortDirection = Default::default();
assert_eq!(default, SortDirection::Asc)
}
#[test]
#[cfg(not(feature = "httpcache"))]
fn header_values() {
let empty = HeaderMap::new();
let actual = get_header_values(&empty);
let expected = (None, None);
assert_eq!(actual, expected);
let mut all_valid = HeaderMap::new();
all_valid.insert(X_RATELIMIT_REMAINING, HeaderValue::from_static("1234"));
all_valid.insert(X_RATELIMIT_RESET, HeaderValue::from_static("5678"));
let actual = get_header_values(&all_valid);
let expected = (Some(1234), Some(5678));
assert_eq!(actual, expected);
let mut invalid = HeaderMap::new();
invalid.insert(X_RATELIMIT_REMAINING, HeaderValue::from_static("foo"));
invalid.insert(X_RATELIMIT_RESET, HeaderValue::from_static("bar"));
let actual = get_header_values(&invalid);
let expected = (None, None);
assert_eq!(actual, expected);
}
#[test]
#[cfg(feature = "httpcache")]
fn header_values() {
let empty = HeaderMap::new();
let actual = get_header_values(&empty);
let expected = (None, None, None);
assert_eq!(actual, expected);
let mut all_valid = HeaderMap::new();
all_valid.insert(X_RATELIMIT_REMAINING, HeaderValue::from_static("1234"));
all_valid.insert(X_RATELIMIT_RESET, HeaderValue::from_static("5678"));
all_valid.insert(ETAG, HeaderValue::from_static("foobar"));
let actual = get_header_values(&all_valid);
let expected = (Some(1234), Some(5678), Some(b"foobar".to_vec()));
assert_eq!(actual, expected);
let mut invalid = HeaderMap::new();
invalid.insert(X_RATELIMIT_REMAINING, HeaderValue::from_static("foo"));
invalid.insert(X_RATELIMIT_RESET, HeaderValue::from_static("bar"));
invalid.insert(ETAG, HeaderValue::from_static(""));
let actual = get_header_values(&invalid);
let expected = (None, None, Some(Vec::new()));
assert_eq!(actual, expected);
}
}