use super::{anyhow, reqwest, semver, serde_json, url, Result};
#[cfg(test)]
use mockito;
use semver::Version;
use std::cell::RefCell;
use url::Url;
#[cfg(not(test))]
const GITHUB_API_URL: &str = "https://api.github.com/repos/";
const GITHUB_LATEST_RELEASE_ENDPOINT: &str = "/releases/latest";
#[cfg(test)]
#[allow(deprecated)]
static MOCKITO_URL: &str = mockito::SERVER_URL;
#[cfg(test)]
pub const MOCK_RELEASER_REPO_NAME: &str = "MockZnVja29mZg==fd850fc2e63511e79f720023dfdf24ec";
pub trait Releaser: Clone {
type SemVersion: Into<Version>;
type DownloadLink: Into<Url>;
fn new<S: Into<String>>(name: S) -> Self;
fn fetch_latest_release(&self) -> Result<(Self::SemVersion, Self::DownloadLink)>;
fn latest_release(&self) -> Result<(Version, Url)> {
let (v, url) = self.fetch_latest_release()?;
Ok((v.into(), url.into()))
}
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GithubReleaser {
repo: String,
latest_release: RefCell<Option<ReleaseItem>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ReleaseItem {
pub tag_name: String,
assets: Vec<ReleaseAsset>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct ReleaseAsset {
url: String,
name: String,
state: String,
browser_download_url: String,
}
impl GithubReleaser {
fn latest_release_data(&self) -> Result<()> {
debug!("starting latest_release_data");
let client = reqwest::blocking::Client::builder()
.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION")
))
.build()?;
#[cfg(test)]
let url = format!("{}{}", MOCKITO_URL, GITHUB_LATEST_RELEASE_ENDPOINT);
#[cfg(not(test))]
let url = format!(
"{}{}{}",
GITHUB_API_URL, self.repo, GITHUB_LATEST_RELEASE_ENDPOINT
);
debug!(" url is: {:?}", url);
client
.get(&url)
.send()?
.error_for_status()
.map_err(Into::into)
.and_then(|resp| {
let mut latest: ReleaseItem = serde_json::from_reader(resp)?;
if latest.tag_name.starts_with('v') {
latest.tag_name.remove(0);
}
debug!(" release item: {:?}", latest);
*self.latest_release.borrow_mut() = Some(latest);
Ok(())
})
}
fn downloadable_url(&self) -> Result<Url> {
debug!("starting download_url");
self.latest_release
.borrow()
.as_ref()
.ok_or_else(|| {
anyhow!(
"no release item available, did you first get version by calling latest_version?",
)
})
.and_then(|r| {
let urls = r
.assets
.iter()
.filter(|asset| {
asset.state == "uploaded"
&& (asset.browser_download_url.ends_with("alfredworkflow")
|| asset.browser_download_url.ends_with("alfred3workflow")
|| asset.browser_download_url.ends_with("alfred4workflow"))
})
.map(|asset| &asset.browser_download_url)
.collect::<Vec<&String>>();
debug!(" collected release urls: {:?}", urls);
match urls.len() {
0 => Err(anyhow!("no usable download url")),
1 => Ok(Url::parse(urls[0])?),
_ => {
let url = urls.iter().find(|item| item.ends_with("alfredworkflow"));
let u = url.unwrap_or(&urls[0]);
Ok(Url::parse(u)?)
}
}
})
}
fn latest_version(&self) -> Result<Version> {
debug!("starting latest_version");
if self.latest_release.borrow().is_none() {
self.latest_release_data()?;
}
let latest_version = self
.latest_release
.borrow()
.as_ref()
.map(|r| Version::parse(&r.tag_name).ok())
.ok_or_else(|| anyhow!("Couldn't parse fetched version."))?
.unwrap();
debug!(" latest version: {:?}", latest_version);
Ok(latest_version)
}
}
impl Releaser for GithubReleaser {
type SemVersion = Version;
type DownloadLink = Url;
fn new<S: Into<String>>(repo_name: S) -> GithubReleaser {
GithubReleaser {
repo: repo_name.into(),
latest_release: RefCell::new(None),
}
}
fn fetch_latest_release(&self) -> Result<(Version, Url)> {
if self.latest_release.borrow().is_none() {
self.latest_release_data()?;
}
let version = self.latest_version()?;
let link = self.downloadable_url()?;
Ok((version, link))
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use mockito::{mock, Matcher, Mock};
#[test]
fn it_tests_releaser() {
let _m = setup_mock_server(200);
let releaser = GithubReleaser::new(MOCK_RELEASER_REPO_NAME);
assert!(releaser.downloadable_url().is_err());
assert!(
releaser
.latest_version()
.expect("couldn't do latest_version")
> Version::new(0, 11, 0)
);
assert_eq!("http://127.0.0.1:1234/releases/download/v0.11.1/alfred-pinboard-rust-v0.11.1.alfredworkflow",
releaser.downloadable_url().unwrap().as_str());
}
pub fn setup_mock_server(status_code: usize) -> Mock {
mock(
"GET",
Matcher::Regex(r"^/releases/(latest|download).*$".to_string()),
)
.with_status(status_code)
.with_header("content-type", "application/json")
.with_body(include_str!("../../tests/latest.json"))
.create()
}
}