Skip to main content

zoi_lua/api/
http.rs

1use mlua::{self, Lua, LuaSerdeExt, Table, Value};
2use zoi_core::utils;
3
4use serde::Deserialize;
5/// Exposes HTTP and Git-forge utilities to the Lua environment.
6///
7/// These functions enable dynamic package definitions by allowing them to:
8/// - `UTILS.FETCH.url`: Fetch raw text content (e.g. version files, checksums).
9/// - `UTILS.FETCH.<FORGE>.LATEST`: Query Git forges (GitHub, GitLab, etc.) for the
10///   latest tags, releases, or commit SHAs.
11///
12/// All network requests respect Zoi's global `--offline` and timeout settings.
13pub fn add_fetch_util(lua: &Lua) -> Result<(), mlua::Error> {
14    let fetch_table = lua.create_table()?;
15
16    let fetch_fn = lua.create_function(|_, url: String| -> Result<String, mlua::Error> {
17        let client =
18            utils::get_http_client().map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
19        let response = client
20            .get(url)
21            .send()
22            .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
23        let text = response
24            .text()
25            .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
26        Ok(text)
27    })?;
28    fetch_table.set("url", fetch_fn)?;
29
30    let utils_table: Table = lua.globals().get("UTILS")?;
31    utils_table.set("FETCH", fetch_table)?;
32
33    Ok(())
34}
35
36#[derive(Deserialize)]
37struct GitArgs {
38    repo: String,
39    domain: Option<String>,
40    branch: Option<String>,
41}
42
43fn fetch_json(url: &str) -> Result<serde_json::Value, mlua::Error> {
44    let client = utils::get_http_client().map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
45
46    let response = client
47        .get(url)
48        .send()
49        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
50
51    if !response.status().is_success() {
52        return Err(mlua::Error::RuntimeError(format!(
53            "Request to {} failed with status: {} and body: {}",
54            url,
55            response.status(),
56            response.text().unwrap_or_else(|_| "N/A".to_string())
57        )));
58    }
59
60    let text = response
61        .text()
62        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
63    serde_json::from_str(&text).map_err(|e| mlua::Error::RuntimeError(e.to_string()))
64}
65
66pub fn add_git_fetch_util(lua: &Lua) -> Result<(), mlua::Error> {
67    let utils_table: Table = lua.globals().get("UTILS")?;
68    let fetch_table: Table = utils_table.get("FETCH")?;
69
70    for provider in ["GITHUB", "GITLAB", "GITEA", "FORGEJO"] {
71        let provider_table = lua.create_table()?;
72        let latest_table = lua.create_table()?;
73
74        for what in ["tag", "release", "commit"] {
75            let get_latest_fn = lua.create_function(move |lua, args: Table| {
76                let git_args: GitArgs = lua
77                    .from_value(Value::Table(args))
78                    .map_err(|e| mlua::Error::RuntimeError(format!("Invalid arguments: {}", e)))?;
79
80                let base_url = match provider {
81                    "GITHUB" => git_args
82                        .domain
83                        .unwrap_or_else(|| "https://api.github.com".to_string()),
84                    "GITLAB" => git_args
85                        .domain
86                        .unwrap_or_else(|| "https://gitlab.com".to_string()),
87                    "GITEA" => git_args
88                        .domain
89                        .unwrap_or_else(|| "https://gitea.com".to_string()),
90                    "FORGEJO" => git_args
91                        .domain
92                        .unwrap_or_else(|| "https://codeberg.org".to_string()),
93                    _ => unreachable!(),
94                };
95
96                let url = match (provider, what) {
97                    ("GITHUB", "tag") => format!("{}/repos/{}/tags", base_url, git_args.repo),
98                    ("GITHUB", "release") => {
99                        format!("{}/repos/{}/releases/latest", base_url, git_args.repo)
100                    }
101                    ("GITHUB", "commit") => format!(
102                        "{}/repos/{}/commits?sha={}",
103                        base_url,
104                        git_args.repo,
105                        git_args.branch.as_deref().unwrap_or("HEAD")
106                    ),
107
108                    ("GITLAB", "tag") => format!(
109                        "{}/api/v4/projects/{}/repository/tags",
110                        base_url,
111                        urlencoding::encode(&git_args.repo)
112                    ),
113                    ("GITLAB", "release") => format!(
114                        "{}/api/v4/projects/{}/releases",
115                        base_url,
116                        urlencoding::encode(&git_args.repo)
117                    ),
118                    ("GITLAB", "commit") => format!(
119                        "{}/api/v4/projects/{}/repository/commits?ref_name={}",
120                        base_url,
121                        urlencoding::encode(&git_args.repo),
122                        git_args.branch.as_deref().unwrap_or("HEAD")
123                    ),
124
125                    ("GITEA" | "FORGEJO", "tag") => {
126                        format!("{}/api/v1/repos/{}/tags", base_url, git_args.repo)
127                    }
128                    ("GITEA" | "FORGEJO", "release") => {
129                        format!(
130                            "{}/api/v1/repos/{}/releases/latest",
131                            base_url, git_args.repo
132                        )
133                    }
134                    ("GITEA" | "FORGEJO", "commit") => format!(
135                        "{}/api/v1/repos/{}/commits?sha={}",
136                        base_url,
137                        git_args.repo,
138                        git_args.branch.as_deref().unwrap_or("HEAD")
139                    ),
140                    _ => unreachable!(),
141                };
142
143                let json = fetch_json(&url)?;
144
145                let result = match (provider, what) {
146                    ("GITHUB", "tag") | ("GITEA", "tag") | ("FORGEJO", "tag") => json
147                        .as_array()
148                        .and_then(|a| a.first())
149                        .and_then(|t| t["name"].as_str()),
150                    ("GITHUB", "release") | ("GITEA", "release") | ("FORGEJO", "release") => {
151                        json["tag_name"].as_str()
152                    }
153                    ("GITHUB", "commit") | ("GITEA", "commit") | ("FORGEJO", "commit") => json
154                        .as_array()
155                        .and_then(|a| a.first())
156                        .and_then(|c| c["sha"].as_str()),
157
158                    ("GITLAB", "tag") => json
159                        .as_array()
160                        .and_then(|a| a.first())
161                        .and_then(|t| t["name"].as_str()),
162                    ("GITLAB", "release") => json
163                        .as_array()
164                        .and_then(|a| a.first())
165                        .and_then(|r| r["tag_name"].as_str()),
166                    ("GITLAB", "commit") => json
167                        .as_array()
168                        .and_then(|a| a.first())
169                        .and_then(|c| c["id"].as_str()),
170                    _ => unreachable!(),
171                };
172
173                result.map(|s| s.to_string()).ok_or_else(|| {
174                    mlua::Error::RuntimeError(
175                        "Could not extract value from API response".to_string(),
176                    )
177                })
178            })?;
179            latest_table.set(what, get_latest_fn)?;
180        }
181
182        provider_table.set("LATEST", latest_table)?;
183        fetch_table.set(provider, provider_table)?;
184    }
185
186    Ok(())
187}