Skip to content

Commit

Permalink
feat: Add GraphQL support to GhApiClient
Browse files Browse the repository at this point in the history
Fixed #868

 - Add new fn `remote::Client::post`
 - Add new fn `remote::RequestBuilder::body`
 - Re-export `reqwest::Body` in `remote`
 - Add dep percent-encoding v2.2.0 to binstalk-downloader
 - Add dep serde-tuple-vec-map v1.0.1 to binstalk-downloader
 - Add GraphQL to `GhApiClient`, fallback to Restful API if token is not
   provided or authorization failed.
 - Fixed `GhReleaseArtifact::try_extract_artifact_from_str`: decode
   percent encoded http url path and add regression tests
 - Added variant `GhApiError::Context` & `GhApiContextError`
 - Added variant `GhApiError::GraphQLErrors` & `GhGraphQLErrors`

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>
  • Loading branch information
NobodyXu committed Jun 4, 2023
1 parent e87e353 commit 93661bf
Show file tree
Hide file tree
Showing 8 changed files with 516 additions and 97 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/binstalk-downloader/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ futures-util = "0.3.28"
generic-array = "0.14.7"
httpdate = "1.0.2"
reqwest = { version = "0.11.18", features = ["stream", "gzip", "brotli", "deflate"], default-features = false }
percent-encoding = "2.2.0"
serde = { version = "1.0.163", features = ["derive"], optional = true }
serde-tuple-vec-map = "1.0.1"
serde_json = { version = "1.0.96", optional = true }
# Use a fork here since we need PAX support, but the upstream
# does not hav the PR merged yet.
Expand Down
209 changes: 143 additions & 66 deletions crates/binstalk-downloader/src/gh_api_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,43 @@ use std::{
time::{Duration, Instant},
};

use compact_str::{CompactString, ToCompactString};
use compact_str::CompactString;
use percent_encoding::{
percent_decode_str, utf8_percent_encode, AsciiSet, PercentEncode, CONTROLS,
};
use tokio::sync::OnceCell;
use tracing::{debug, warn};

use crate::remote;

mod request;
pub use request::GhApiError;
pub use request::{GhApiContextError, GhApiError, GhGraphQLErrors};

/// default retry duration if x-ratelimit-reset is not found in response header
const DEFAULT_RETRY_DURATION: Duration = Duration::from_secs(3);

fn percent_encode_http_url_path(path: &str) -> PercentEncode<'_> {
/// https://url.spec.whatwg.org/#fragment-percent-encode-set
const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');

/// https://url.spec.whatwg.org/#path-percent-encode-set
const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');

const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'%');

// The backslash (\) character is treated as a path separator in special URLs
// so it needs to be additionally escaped in that case.
//
// http is considered to have special path.
const SPECIAL_PATH_SEGMENT: &AsciiSet = &PATH_SEGMENT.add(b'\\');

utf8_percent_encode(path, SPECIAL_PATH_SEGMENT)
}

fn percent_decode_http_url_path(input: &str) -> CompactString {
percent_decode_str(input).decode_utf8_lossy().into()
}

/// The keys required to identify a github release.
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct GhRelease {
Expand Down Expand Up @@ -57,11 +82,11 @@ impl GhReleaseArtifact {
(path_segments.next().is_none() && url.fragment().is_none() && url.query().is_none()).then(
|| Self {
release: GhRelease {
owner: owner.to_compact_string(),
repo: repo.to_compact_string(),
tag: tag.to_compact_string(),
owner: percent_decode_http_url_path(owner),
repo: percent_decode_http_url_path(repo),
tag: percent_decode_http_url_path(tag),
},
artifact_name: artifact_name.to_compact_string(),
artifact_name: percent_decode_http_url_path(artifact_name),
},
)
}
Expand Down Expand Up @@ -258,6 +283,8 @@ pub enum HasReleaseArtifact {
#[cfg(test)]
mod test {
use super::*;
use compact_str::{CompactString, ToCompactString};
use std::env;

mod cargo_binstall_v0_20_1 {
use super::{CompactString, GhRelease};
Expand Down Expand Up @@ -347,94 +374,144 @@ mod test {

/// Mark this as an async fn so that you won't accidentally use it in
/// sync context.
async fn create_client() -> GhApiClient {
GhApiClient::new(
remote::Client::new(
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
None,
Duration::from_millis(10),
1.try_into().unwrap(),
[],
)
.unwrap(),
async fn create_client() -> Vec<GhApiClient> {
let client = remote::Client::new(
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
None,
Duration::from_millis(10),
1.try_into().unwrap(),
[],
)
}
.unwrap();

#[tokio::test]
async fn test_gh_api_client_cargo_binstall_v0_20_1() {
let client = create_client().await;
let mut gh_clients = vec![GhApiClient::new(client.clone(), None)];

let release = cargo_binstall_v0_20_1::RELEASE;
if let Ok(token) = env::var("GITHUB_TOKEN") {
gh_clients.push(GhApiClient::new(client, Some(token.into())));
}

let artifacts = cargo_binstall_v0_20_1::ARTIFACTS
.iter()
.map(ToCompactString::to_compact_string);
gh_clients
}

async fn test_specific_release(release: &GhRelease, artifacts: &[&str]) {
for client in create_client().await {
eprintln!("In client {client:?}");

for artifact_name in artifacts {
let ret = client
.has_release_artifact(GhReleaseArtifact {
release: release.clone(),
artifact_name: artifact_name.to_compact_string(),
})
.await
.unwrap();

assert!(
matches!(
ret,
HasReleaseArtifact::Yes | HasReleaseArtifact::RateLimit { .. }
),
"for '{artifact_name}': answer is {:#?}",
ret
);
}

for artifact_name in artifacts {
let ret = client
.has_release_artifact(GhReleaseArtifact {
release: release.clone(),
artifact_name,
artifact_name: "123z".to_compact_string(),
})
.await
.unwrap();

assert!(
matches!(
ret,
HasReleaseArtifact::Yes | HasReleaseArtifact::RateLimit { .. }
HasReleaseArtifact::No | HasReleaseArtifact::RateLimit { .. }
),
"ret = {:#?}",
ret
);
}
}

let ret = client
.has_release_artifact(GhReleaseArtifact {
release,
artifact_name: "123z".to_compact_string(),
})
.await
.unwrap();

assert!(
matches!(
ret,
HasReleaseArtifact::No | HasReleaseArtifact::RateLimit { .. }
),
"ret = {:#?}",
ret
);
#[tokio::test]
async fn test_gh_api_client_cargo_binstall_v0_20_1() {
test_specific_release(
&cargo_binstall_v0_20_1::RELEASE,
cargo_binstall_v0_20_1::ARTIFACTS,
)
.await
}

#[tokio::test]
async fn test_gh_api_client_cargo_binstall_no_such_release() {
let client = create_client().await;

let release = GhRelease {
owner: "cargo-bins".to_compact_string(),
repo: "cargo-binstall".to_compact_string(),
// We are currently at v0.20.1 and we would never release
// anything older than v0.20.1
tag: "v0.18.2".to_compact_string(),
for client in create_client().await {
let release = GhRelease {
owner: "cargo-bins".to_compact_string(),
repo: "cargo-binstall".to_compact_string(),
// We are currently at v0.20.1 and we would never release
// anything older than v0.20.1
tag: "v0.18.2".to_compact_string(),
};

let ret = client
.has_release_artifact(GhReleaseArtifact {
release,
artifact_name: "1234".to_compact_string(),
})
.await
.unwrap();

assert!(
matches!(
ret,
HasReleaseArtifact::NoSuchRelease | HasReleaseArtifact::RateLimit { .. }
),
"ret = {:#?}",
ret
);
}
}

mod cargo_audit_v_0_17_6 {
use super::*;

const RELEASE: GhRelease = GhRelease {
owner: CompactString::new_inline("rustsec"),
repo: CompactString::new_inline("rustsec"),
tag: CompactString::new_inline("cargo-audit/v0.17.6"),
};

let ret = client
.has_release_artifact(GhReleaseArtifact {
release,
artifact_name: "1234".to_compact_string(),
})
.await
.unwrap();
const ARTIFACTS: &[&str] = &[
"cargo-audit-aarch64-unknown-linux-gnu-v0.17.6.tgz",
"cargo-audit-armv7-unknown-linux-gnueabihf-v0.17.6.tgz",
"cargo-audit-x86_64-apple-darwin-v0.17.6.tgz",
"cargo-audit-x86_64-pc-windows-msvc-v0.17.6.zip",
"cargo-audit-x86_64-unknown-linux-gnu-v0.17.6.tgz",
"cargo-audit-x86_64-unknown-linux-gnu-v0.17.6.tgz",
];

#[test]
fn extract_with_escaped_characters() {
let release_artifact = try_extract_artifact_from_str(
"https://github.com/rustsec/rustsec/releases/download/cargo-audit%2Fv0.17.6/cargo-audit-aarch64-unknown-linux-gnu-v0.17.6.tgz"
).unwrap();

assert_eq!(
release_artifact,
GhReleaseArtifact {
release: RELEASE,
artifact_name: CompactString::from(
"cargo-audit-aarch64-unknown-linux-gnu-v0.17.6.tgz",
)
}
);
}

assert!(
matches!(
ret,
HasReleaseArtifact::NoSuchRelease | HasReleaseArtifact::RateLimit { .. }
),
"ret = {:#?}",
ret
);
#[tokio::test]
async fn test_gh_api_client_cargo_audit_v_0_17_6() {
test_specific_release(&RELEASE, ARTIFACTS).await
}
}
}
Loading

0 comments on commit 93661bf

Please sign in to comment.