Skip to main content

modde_games/tools/
release.rs

1//! Queries the GitHub releases API to enumerate tool releases and their assets,
2//! mapping them into [`ToolReleaseSummary`] values for tool installation.
3
4use anyhow::Result;
5use reqwest::Client;
6use serde::Deserialize;
7
8use super::{ToolReleaseAsset, ToolReleaseSummary};
9
10#[derive(Debug, Deserialize)]
11struct GitHubRelease {
12    tag_name: Option<String>,
13    name: Option<String>,
14    published_at: Option<String>,
15    assets: Vec<GitHubReleaseAsset>,
16}
17
18#[derive(Debug, Deserialize)]
19struct GitHubReleaseAsset {
20    name: String,
21    browser_download_url: String,
22    size: u64,
23}
24
25pub(crate) async fn github_json<T: for<'de> Deserialize<'de>>(
26    client: &Client,
27    url: &str,
28) -> Result<T> {
29    let mut request = client.get(url).header("User-Agent", "modde");
30    if let Ok(token) = std::env::var("GITHUB_TOKEN") {
31        request = request.header("Authorization", format!("Bearer {token}"));
32    }
33    Ok(request.send().await?.error_for_status()?.json().await?)
34}
35
36pub async fn list_github_releases(repo: &str) -> Result<Vec<ToolReleaseSummary>> {
37    let client = Client::new();
38    let releases: Vec<GitHubRelease> = github_json(
39        &client,
40        &format!("https://api.github.com/repos/{repo}/releases?per_page=100"),
41    )
42    .await?;
43    Ok(releases
44        .into_iter()
45        .filter_map(|release| {
46            Some(ToolReleaseSummary {
47                tag: release.tag_name?,
48                name: release.name,
49                published_at: release.published_at,
50                assets: release
51                    .assets
52                    .into_iter()
53                    .map(|asset| ToolReleaseAsset {
54                        name: asset.name,
55                        download_url: asset.browser_download_url,
56                        size: asset.size,
57                    })
58                    .collect(),
59            })
60        })
61        .collect())
62}
63
64#[must_use]
65pub fn prepend_latest_dedup<I>(values: I) -> Vec<String>
66where
67    I: IntoIterator<Item = String>,
68{
69    let mut out = vec!["latest".to_string()];
70    for value in values {
71        if value.trim().is_empty() || out.iter().any(|existing| existing == &value) {
72            continue;
73        }
74        out.push(value);
75    }
76    out
77}