use crate::data;
use anyhow::anyhow;
use anyhow::Result;
use curl::easy::{Easy, List};
use serde::{Deserialize, Serialize};
use std::sync::MutexGuard;
const DEFAULT_GITHUB_URL: &str = "https://api.github.com";
#[derive(Deserialize, Serialize, Debug)]
struct ReleasesResponse {
#[serde(rename = "tag_name")]
tag_name: String,
#[serde(rename = "assets")]
assets: Vec<ReleaseAssetResponse>,
}
#[derive(Deserialize, Serialize, Debug)]
struct ReleaseAssetResponse {
#[serde(rename = "browser_download_url")]
browser_download_url: String,
}
pub struct GitHubVendor {
base_url: String,
owner: String,
repo: String,
}
impl GitHubVendor {
pub fn new(owner: &str, repo: &str) -> Self {
Self::custom(owner, repo, None)
}
pub fn custom(owner: &str, repo: &str, base_url: Option<String>) -> Self {
Self {
base_url: base_url.unwrap_or_else(|| DEFAULT_GITHUB_URL.to_string()),
owner: owner.to_string(),
repo: repo.to_string(),
}
}
fn default_headers() -> Result<List> {
let mut headers = List::new();
headers.append("accept: application/vnd.github.v3+json")?;
Ok(headers)
}
}
impl data::Vendor for GitHubVendor {
fn get(&self, client: MutexGuard<Easy>) -> Result<data::Release> {
let mut json = Vec::new();
let mut client = client;
let url = format!(
"{}/repos/{}/{}/releases?per_page=1",
self.base_url, self.owner, self.repo
);
client.url(&url)?;
client.http_headers(Self::default_headers()?)?;
{
let mut transfer = client.transfer();
transfer
.write_function(|data| {
json.extend_from_slice(data);
Ok(data.len())
})
.unwrap();
transfer.perform()?;
}
let response: Vec<ReleasesResponse> = serde_json::from_slice(&json)?;
if response.is_empty() {
return Err(anyhow!("releases not found"));
}
let release_details = response.first().unwrap();
let download_releases = &release_details
.assets
.iter()
.map(|asset| asset.browser_download_url.to_string())
.collect::<Vec<_>>();
Ok(data::Release {
version: release_details.tag_name.to_string(),
downloads_releases: download_releases.clone(),
})
}
}
#[cfg(test)]
mod vendor_github_github {
use crate::data::Vendor;
use super::{Easy, GitHubVendor};
use insta::assert_debug_snapshot;
use std::sync::{Arc, Mutex};
#[test]
fn can_get_release_details() {
let url = &mockito::server_url();
let github = GitHubVendor::custom("owner", "repo", Some(url.to_string()));
let data = r#"[
{
"tag_name": "v0.1.6",
"assets": [
{
"browser_download_url": "https://github.com/foo"
},
{
"browser_download_url": "https://github.com/bar"
}
]
}
]
"#;
let _m = mockito::mock("GET", "/repos/owner/repo/releases?per_page=1")
.match_header("accept", "application/vnd.github.v3+json")
.with_body(data)
.with_status(200)
.create();
let easy = Easy::new();
assert_debug_snapshot!(github.get(Arc::new(Mutex::new(easy)).lock().unwrap()));
}
#[test]
fn can_get_release_details_without_releases() {
let url = &mockito::server_url();
let github = GitHubVendor::custom("owner", "repo", Some(url.to_string()));
let _m = mockito::mock("GET", "/repos/owner/repo/releases?per_page=1")
.match_header("accept", "application/vnd.github.v3+json")
.with_body("[]")
.with_status(200)
.create();
let easy = Easy::new();
assert_debug_snapshot!(github.get(Arc::new(Mutex::new(easy)).lock().unwrap()));
}
}