use anyhow::{Context, Result};
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct ReleaseInfo {
pub tag_name: String,
#[serde(default)]
#[allow(dead_code)]
pub prerelease: bool,
}
pub fn owner() -> String {
std::env::var("MNEMO_OWNER").unwrap_or_else(|_| "Vesperis-group".to_string())
}
pub fn repo() -> String {
std::env::var("MNEMO_REPO").unwrap_or_else(|_| "mnemo".to_string())
}
pub fn api_base() -> String {
std::env::var("MNEMO_GITHUB_API")
.unwrap_or_else(|_| "https://api.github.com".to_string())
.trim_end_matches('/')
.to_string()
}
pub fn download_base() -> String {
std::env::var("MNEMO_GITHUB_BASE")
.unwrap_or_else(|_| "https://github.com".to_string())
.trim_end_matches('/')
.to_string()
}
pub fn parse_github_latest_release(json: &str) -> Result<ReleaseInfo> {
let info: ReleaseInfo =
serde_json::from_str(json).context("réponse GitHub illisible (JSON invalide)")?;
if info.tag_name.trim().is_empty() {
anyhow::bail!("réponse GitHub sans `tag_name`");
}
Ok(info)
}
pub fn asset_url(tag: &str, file_name: &str) -> String {
format!(
"{}/{}/{}/releases/download/{}/{}",
download_base(),
owner(),
repo(),
tag,
file_name
)
}
pub fn fetch_latest_release() -> Result<ReleaseInfo> {
let url = format!(
"{}/repos/{}/{}/releases/latest",
api_base(),
owner(),
repo()
);
let body = http_get_string(&url).with_context(|| format!("échec de la requête vers {url}"))?;
parse_github_latest_release(&body)
}
pub fn http_get_string(url: &str) -> Result<String> {
let mut resp = ureq::get(url)
.header("User-Agent", "mnemo-cli")
.header("Accept", "application/vnd.github+json")
.call()
.map_err(|err| map_ureq_error(url, err))?;
resp.body_mut()
.read_to_string()
.context("corps de réponse HTTP illisible")
}
pub fn http_get_bytes(url: &str) -> Result<Vec<u8>> {
let mut resp = ureq::get(url)
.header("User-Agent", "mnemo-cli")
.call()
.map_err(|err| map_ureq_error(url, err))?;
let mut buf = Vec::new();
use std::io::Read;
resp.body_mut()
.as_reader()
.read_to_end(&mut buf)
.context("téléchargement interrompu")?;
Ok(buf)
}
fn map_ureq_error(url: &str, err: ureq::Error) -> anyhow::Error {
match err {
ureq::Error::StatusCode(code) => {
anyhow::anyhow!("réponse HTTP {code} pour {url}")
}
other => anyhow::anyhow!("erreur réseau pour {url} : {other}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_release_stable() {
let json = r#"{"tag_name":"v0.5.0","prerelease":false,"name":"v0.5.0"}"#;
let info = parse_github_latest_release(json).unwrap();
assert_eq!(info.tag_name, "v0.5.0");
assert!(!info.prerelease);
}
#[test]
fn parse_release_prerelease() {
let json = r#"{"tag_name":"v0.6.0-rc1","prerelease":true}"#;
let info = parse_github_latest_release(json).unwrap();
assert_eq!(info.tag_name, "v0.6.0-rc1");
assert!(info.prerelease);
}
#[test]
fn parse_release_json_invalide() {
assert!(parse_github_latest_release("pas du json").is_err());
assert!(parse_github_latest_release(r#"{"name":"x"}"#).is_err());
assert!(parse_github_latest_release(r#"{"tag_name":""}"#).is_err());
}
#[test]
fn url_asset_construite() {
std::env::set_var("MNEMO_GITHUB_BASE", "http://localhost:9");
std::env::set_var("MNEMO_OWNER", "acme");
std::env::set_var("MNEMO_REPO", "tool");
let url = asset_url("v0.5.0", "mnemo-v0.5.0-x86_64-unknown-linux-musl.tar.gz");
assert_eq!(
url,
"http://localhost:9/acme/tool/releases/download/v0.5.0/mnemo-v0.5.0-x86_64-unknown-linux-musl.tar.gz"
);
std::env::remove_var("MNEMO_GITHUB_BASE");
std::env::remove_var("MNEMO_OWNER");
std::env::remove_var("MNEMO_REPO");
}
}