mnemo/lifecycle/
github.rs1use anyhow::{Context, Result};
8use serde::Deserialize;
9
10#[derive(Debug, Clone, Deserialize)]
12pub struct ReleaseInfo {
13 pub tag_name: String,
15 #[serde(default)]
18 #[allow(dead_code)]
19 pub prerelease: bool,
20}
21
22pub fn owner() -> String {
24 std::env::var("MNEMO_OWNER").unwrap_or_else(|_| "Vesperis-group".to_string())
25}
26
27pub fn repo() -> String {
29 std::env::var("MNEMO_REPO").unwrap_or_else(|_| "mnemo".to_string())
30}
31
32pub fn api_base() -> String {
35 std::env::var("MNEMO_GITHUB_API")
36 .unwrap_or_else(|_| "https://api.github.com".to_string())
37 .trim_end_matches('/')
38 .to_string()
39}
40
41pub fn download_base() -> String {
43 std::env::var("MNEMO_GITHUB_BASE")
44 .unwrap_or_else(|_| "https://github.com".to_string())
45 .trim_end_matches('/')
46 .to_string()
47}
48
49pub fn parse_github_latest_release(json: &str) -> Result<ReleaseInfo> {
51 let info: ReleaseInfo =
52 serde_json::from_str(json).context("réponse GitHub illisible (JSON invalide)")?;
53 if info.tag_name.trim().is_empty() {
54 anyhow::bail!("réponse GitHub sans `tag_name`");
55 }
56 Ok(info)
57}
58
59pub fn asset_url(tag: &str, file_name: &str) -> String {
61 format!(
62 "{}/{}/{}/releases/download/{}/{}",
63 download_base(),
64 owner(),
65 repo(),
66 tag,
67 file_name
68 )
69}
70
71pub fn fetch_latest_release() -> Result<ReleaseInfo> {
75 let url = format!(
76 "{}/repos/{}/{}/releases/latest",
77 api_base(),
78 owner(),
79 repo()
80 );
81 let body = http_get_string(&url).with_context(|| format!("échec de la requête vers {url}"))?;
82 parse_github_latest_release(&body)
83}
84
85pub fn http_get_string(url: &str) -> Result<String> {
87 let mut resp = ureq::get(url)
88 .header("User-Agent", "mnemo-cli")
89 .header("Accept", "application/vnd.github+json")
90 .call()
91 .map_err(|err| map_ureq_error(url, err))?;
92 resp.body_mut()
93 .read_to_string()
94 .context("corps de réponse HTTP illisible")
95}
96
97pub fn http_get_bytes(url: &str) -> Result<Vec<u8>> {
99 let mut resp = ureq::get(url)
100 .header("User-Agent", "mnemo-cli")
101 .call()
102 .map_err(|err| map_ureq_error(url, err))?;
103 let mut buf = Vec::new();
104 use std::io::Read;
105 resp.body_mut()
109 .as_reader()
110 .read_to_end(&mut buf)
111 .context("téléchargement interrompu")?;
112 Ok(buf)
113}
114
115fn map_ureq_error(url: &str, err: ureq::Error) -> anyhow::Error {
117 match err {
118 ureq::Error::StatusCode(code) => {
119 anyhow::anyhow!("réponse HTTP {code} pour {url}")
120 }
121 other => anyhow::anyhow!("erreur réseau pour {url} : {other}"),
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn parse_release_stable() {
131 let json = r#"{"tag_name":"v0.5.0","prerelease":false,"name":"v0.5.0"}"#;
132 let info = parse_github_latest_release(json).unwrap();
133 assert_eq!(info.tag_name, "v0.5.0");
134 assert!(!info.prerelease);
135 }
136
137 #[test]
138 fn parse_release_prerelease() {
139 let json = r#"{"tag_name":"v0.6.0-rc1","prerelease":true}"#;
140 let info = parse_github_latest_release(json).unwrap();
141 assert_eq!(info.tag_name, "v0.6.0-rc1");
142 assert!(info.prerelease);
143 }
144
145 #[test]
146 fn parse_release_json_invalide() {
147 assert!(parse_github_latest_release("pas du json").is_err());
148 assert!(parse_github_latest_release(r#"{"name":"x"}"#).is_err());
149 assert!(parse_github_latest_release(r#"{"tag_name":""}"#).is_err());
150 }
151
152 #[test]
153 fn url_asset_construite() {
154 std::env::set_var("MNEMO_GITHUB_BASE", "http://localhost:9");
155 std::env::set_var("MNEMO_OWNER", "acme");
156 std::env::set_var("MNEMO_REPO", "tool");
157 let url = asset_url("v0.5.0", "mnemo-v0.5.0-x86_64-unknown-linux-musl.tar.gz");
158 assert_eq!(
159 url,
160 "http://localhost:9/acme/tool/releases/download/v0.5.0/mnemo-v0.5.0-x86_64-unknown-linux-musl.tar.gz"
161 );
162 std::env::remove_var("MNEMO_GITHUB_BASE");
163 std::env::remove_var("MNEMO_OWNER");
164 std::env::remove_var("MNEMO_REPO");
165 }
166}