use crate::{HTTPJSONError, UpstreamDatum};
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION};
const API_BASE: &str = "https://api.github.com";
const RAW_BASE: &str = "https://raw.githubusercontent.com";
fn auth_headers(accept: &'static str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(ACCEPT, HeaderValue::from_static(accept));
if let Ok(token) = std::env::var("GITHUB_TOKEN") {
if let Ok(value) = HeaderValue::from_str(&format!("Bearer {}", token)) {
headers.insert(AUTHORIZATION, value);
}
}
headers
}
async fn fetch(url: &str, accept: &'static str) -> Result<reqwest::Response, HTTPJSONError> {
let client = crate::http::build_client()
.default_headers(auth_headers(accept))
.build()
.map_err(HTTPJSONError::HTTPError)?;
let response = client
.get(url)
.send()
.await
.map_err(HTTPJSONError::HTTPError)?;
if !response.status().is_success() {
return Err(HTTPJSONError::Error {
url: response.url().clone(),
status: response.status().as_u16(),
response: Box::new(response),
});
}
Ok(response)
}
pub async fn load_github_json(path: &str) -> Result<serde_json::Value, HTTPJSONError> {
let url = format!("{}/{}", API_BASE, path.trim_start_matches('/'));
let response = fetch(&url, "application/vnd.github+json").await?;
response.json().await.map_err(HTTPJSONError::HTTPError)
}
pub async fn download_raw_file(
owner: &str,
repo: &str,
reference: &str,
path: &str,
) -> Result<String, HTTPJSONError> {
let url = format!(
"{}/{}/{}/{}/{}",
RAW_BASE,
owner,
repo,
reference,
path.trim_start_matches('/')
);
let response = fetch(&url, "text/plain").await?;
response.text().await.map_err(HTTPJSONError::HTTPError)
}
pub async fn download_contents(
owner: &str,
repo: &str,
path: &str,
reference: Option<&str>,
) -> Result<Vec<u8>, crate::ProviderError> {
let mut url = format!(
"{}/repos/{}/{}/contents/{}",
API_BASE,
owner,
repo,
path.trim_start_matches('/')
);
if let Some(reference) = reference {
url.push_str(&format!("?ref={}", reference));
}
let response = fetch(&url, "application/vnd.github+json").await?;
let data: serde_json::Value = response.json().await.map_err(HTTPJSONError::HTTPError)?;
let encoding = data["encoding"].as_str();
let content = data["content"].as_str().ok_or_else(|| {
crate::ProviderError::ParseError("contents API response missing content".to_string())
})?;
match encoding {
Some("base64") => {
use base64::Engine;
let stripped: String = content.chars().filter(|c| !c.is_whitespace()).collect();
base64::engine::general_purpose::STANDARD
.decode(stripped.as_bytes())
.map_err(|e| {
crate::ProviderError::ParseError(format!("invalid base64 content: {}", e))
})
}
other => Err(crate::ProviderError::ParseError(format!(
"unexpected contents encoding: {:?}",
other
))),
}
}
#[derive(serde::Deserialize, Debug, Clone)]
pub struct RepoMetadata {
pub description: Option<String>,
pub homepage: Option<String>,
pub html_url: Option<String>,
pub license: Option<License>,
#[serde(default)]
pub archived: bool,
}
#[derive(serde::Deserialize, Debug, Clone)]
pub struct License {
pub spdx_id: Option<String>,
}
pub async fn repo_metadata(
owner: &str,
repo: &str,
) -> Result<Option<RepoMetadata>, crate::ProviderError> {
let path = format!("repos/{}/{}", owner, repo);
match load_github_json(&path).await {
Ok(value) => serde_json::from_value(value).map(Some).map_err(|e| {
crate::ProviderError::ParseError(format!("Failed to parse repo metadata: {}", e))
}),
Err(HTTPJSONError::Error { status: 404, .. }) => Ok(None),
Err(e) => Err(e.into()),
}
}
impl RepoMetadata {
pub fn to_upstream_data(&self) -> Vec<UpstreamDatum> {
let mut results = Vec::new();
if let Some(description) = self.description.as_deref() {
if !description.is_empty() {
results.push(UpstreamDatum::Summary(description.to_string()));
}
}
if let Some(homepage) = self.homepage.as_deref() {
if !homepage.is_empty() {
results.push(UpstreamDatum::Homepage(homepage.to_string()));
}
}
if let Some(html_url) = self.html_url.as_deref() {
results.push(UpstreamDatum::Repository(html_url.to_string()));
}
if let Some(spdx) = self.license.as_ref().and_then(|l| l.spdx_id.as_deref()) {
if !spdx.is_empty() && spdx != "NOASSERTION" {
results.push(UpstreamDatum::License(spdx.to_string()));
}
}
results
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_repo_metadata() {
let data = r#"{
"description": "Serialization framework for Rust",
"homepage": "https://serde.rs/",
"html_url": "https://github.com/serde-rs/serde",
"license": {"spdx_id": "Apache-2.0"},
"archived": false
}"#;
let meta: RepoMetadata = serde_json::from_str(data).unwrap();
assert_eq!(
meta.description.as_deref(),
Some("Serialization framework for Rust")
);
assert_eq!(
meta.license.as_ref().unwrap().spdx_id.as_deref(),
Some("Apache-2.0")
);
let data = meta.to_upstream_data();
assert_eq!(
data,
vec![
UpstreamDatum::Summary("Serialization framework for Rust".to_string()),
UpstreamDatum::Homepage("https://serde.rs/".to_string()),
UpstreamDatum::Repository("https://github.com/serde-rs/serde".to_string()),
UpstreamDatum::License("Apache-2.0".to_string()),
]
);
}
#[test]
fn test_noassertion_license_dropped() {
let data = r#"{"license": {"spdx_id": "NOASSERTION"}}"#;
let meta: RepoMetadata = serde_json::from_str(data).unwrap();
assert_eq!(meta.to_upstream_data(), vec![]);
}
}