Skip to main content

zoi_lua/api/
http.rs

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