sps_net/
api.rs

1use std::sync::Arc;
2
3use reqwest::header::{ACCEPT, AUTHORIZATION, USER_AGENT};
4use reqwest::Client;
5use serde_json::Value;
6use sps_common::config::Config;
7use sps_common::error::{Result, SpsError};
8use sps_common::model::cask::{Cask, CaskList};
9use sps_common::model::formula::Formula;
10use tracing::{debug, error};
11
12const FORMULAE_API_BASE_URL: &str = "https://formulae.brew.sh/api";
13const GITHUB_API_BASE_URL: &str = "https://api.github.com";
14const USER_AGENT_STRING: &str = "sps Package Manager (Rust; +https://github.com/your/sp)";
15
16fn build_api_client(config: &Config) -> Result<Client> {
17    let mut headers = reqwest::header::HeaderMap::new();
18    headers.insert(USER_AGENT, USER_AGENT_STRING.parse().unwrap());
19    headers.insert(ACCEPT, "application/vnd.github+json".parse().unwrap());
20    if let Some(token) = &config.github_api_token {
21        debug!("Adding GitHub API token to request headers.");
22        match format!("Bearer {token}").parse() {
23            Ok(val) => {
24                headers.insert(AUTHORIZATION, val);
25            }
26            Err(e) => {
27                error!("Failed to parse GitHub API token into header value: {}", e);
28            }
29        }
30    } else {
31        debug!("No GitHub API token found in config.");
32    }
33    Ok(Client::builder().default_headers(headers).build()?)
34}
35
36pub async fn fetch_raw_formulae_json(endpoint: &str) -> Result<String> {
37    let url = format!("{FORMULAE_API_BASE_URL}/{endpoint}");
38    debug!("Fetching data from Homebrew Formulae API: {}", url);
39    let client = reqwest::Client::builder()
40        .user_agent(USER_AGENT_STRING)
41        .build()?;
42    let response = client.get(&url).send().await.map_err(|e| {
43        debug!("HTTP request failed for {}: {}", url, e);
44        SpsError::Http(Arc::new(e))
45    })?;
46    if !response.status().is_success() {
47        let status = response.status();
48        let body = response
49            .text()
50            .await
51            .unwrap_or_else(|e| format!("(Failed to read response body: {e})"));
52        debug!(
53            "HTTP request to {} returned non-success status: {}",
54            url, status
55        );
56        debug!("Response body for failed request to {}: {}", url, body);
57        return Err(SpsError::Api(format!("HTTP status {status} from {url}")));
58    }
59    let body = response.text().await?;
60    if body.trim().is_empty() {
61        error!("Response body for {} was empty.", url);
62        return Err(SpsError::Api(format!(
63            "Empty response body received from {url}"
64        )));
65    }
66    Ok(body)
67}
68
69pub async fn fetch_all_formulas() -> Result<String> {
70    fetch_raw_formulae_json("formula.json").await
71}
72
73pub async fn fetch_all_casks() -> Result<String> {
74    fetch_raw_formulae_json("cask.json").await
75}
76
77pub async fn fetch_formula(name: &str) -> Result<serde_json::Value> {
78    let direct_fetch_result = fetch_raw_formulae_json(&format!("formula/{name}.json")).await;
79    if let Ok(body) = direct_fetch_result {
80        let formula: serde_json::Value = serde_json::from_str(&body)?;
81        Ok(formula)
82    } else {
83        debug!(
84            "Direct fetch for formula '{}' failed ({:?}). Fetching full list as fallback.",
85            name,
86            direct_fetch_result.err()
87        );
88        let all_formulas_body = fetch_all_formulas().await?;
89        let formulas: Vec<serde_json::Value> = serde_json::from_str(&all_formulas_body)?;
90        for formula in formulas {
91            if formula.get("name").and_then(Value::as_str) == Some(name) {
92                return Ok(formula);
93            }
94            if formula.get("full_name").and_then(Value::as_str) == Some(name) {
95                return Ok(formula);
96            }
97        }
98        Err(SpsError::NotFound(format!(
99            "Formula '{name}' not found in API list"
100        )))
101    }
102}
103
104pub async fn fetch_cask(token: &str) -> Result<serde_json::Value> {
105    let direct_fetch_result = fetch_raw_formulae_json(&format!("cask/{token}.json")).await;
106    if let Ok(body) = direct_fetch_result {
107        let cask: serde_json::Value = serde_json::from_str(&body)?;
108        Ok(cask)
109    } else {
110        debug!(
111            "Direct fetch for cask '{}' failed ({:?}). Fetching full list as fallback.",
112            token,
113            direct_fetch_result.err()
114        );
115        let all_casks_body = fetch_all_casks().await?;
116        let casks: Vec<serde_json::Value> = serde_json::from_str(&all_casks_body)?;
117        for cask in casks {
118            if cask.get("token").and_then(Value::as_str) == Some(token) {
119                return Ok(cask);
120            }
121        }
122        Err(SpsError::NotFound(format!(
123            "Cask '{token}' not found in API list"
124        )))
125    }
126}
127
128async fn fetch_github_api_json(endpoint: &str, config: &Config) -> Result<Value> {
129    let url = format!("{GITHUB_API_BASE_URL}{endpoint}");
130    debug!("Fetching data from GitHub API: {}", url);
131    let client = build_api_client(config)?;
132    let response = client.get(&url).send().await.map_err(|e| {
133        error!("GitHub API request failed for {}: {}", url, e);
134        SpsError::Http(Arc::new(e))
135    })?;
136    if !response.status().is_success() {
137        let status = response.status();
138        let body = response
139            .text()
140            .await
141            .unwrap_or_else(|e| format!("(Failed to read response body: {e})"));
142        error!(
143            "GitHub API request to {} returned non-success status: {}",
144            url, status
145        );
146        debug!(
147            "Response body for failed GitHub API request to {}: {}",
148            url, body
149        );
150        return Err(SpsError::Api(format!("HTTP status {status} from {url}")));
151    }
152    let value: Value = response.json::<Value>().await.map_err(|e| {
153        error!("Failed to parse JSON response from {}: {}", url, e);
154        SpsError::ApiRequestError(e.to_string())
155    })?;
156    Ok(value)
157}
158
159#[allow(dead_code)]
160async fn fetch_github_repo_info(owner: &str, repo: &str, config: &Config) -> Result<Value> {
161    let endpoint = format!("/repos/{owner}/{repo}");
162    fetch_github_api_json(&endpoint, config).await
163}
164
165pub async fn get_formula(name: &str) -> Result<Formula> {
166    let url = format!("{FORMULAE_API_BASE_URL}/formula/{name}.json");
167    debug!(
168        "Fetching and parsing formula data for '{}' from {}",
169        name, url
170    );
171    let client = reqwest::Client::new();
172    let response = client.get(&url).send().await.map_err(|e| {
173        debug!("HTTP request failed when fetching formula {}: {}", name, e);
174        SpsError::Http(Arc::new(e))
175    })?;
176    let status = response.status();
177    let text = response.text().await?;
178    if !status.is_success() {
179        debug!("Failed to fetch formula {} (Status {})", name, status);
180        debug!("Response body for failed formula fetch {}: {}", name, text);
181        return Err(SpsError::Api(format!(
182            "Failed to fetch formula {name}: Status {status}"
183        )));
184    }
185    if text.trim().is_empty() {
186        error!("Received empty body when fetching formula {}", name);
187        return Err(SpsError::Api(format!(
188            "Empty response body for formula {name}"
189        )));
190    }
191    match serde_json::from_str::<Formula>(&text) {
192        Ok(formula) => Ok(formula),
193        Err(_) => match serde_json::from_str::<Vec<Formula>>(&text) {
194            Ok(mut formulas) if !formulas.is_empty() => {
195                debug!(
196                    "Parsed formula {} from a single-element array response.",
197                    name
198                );
199                Ok(formulas.remove(0))
200            }
201            Ok(_) => {
202                error!("Received empty array when fetching formula {}", name);
203                Err(SpsError::NotFound(format!(
204                    "Formula '{name}' not found (empty array returned)"
205                )))
206            }
207            Err(e_vec) => {
208                error!(
209                    "Failed to parse formula {} as object or array. Error: {}. Body (sample): {}",
210                    name,
211                    e_vec,
212                    text.chars().take(500).collect::<String>()
213                );
214                Err(SpsError::Json(Arc::new(e_vec)))
215            }
216        },
217    }
218}
219
220pub async fn get_all_formulas() -> Result<Vec<Formula>> {
221    let raw_data = fetch_all_formulas().await?;
222    serde_json::from_str(&raw_data).map_err(|e| {
223        error!("Failed to parse all_formulas response: {}", e);
224        SpsError::Json(Arc::new(e))
225    })
226}
227
228pub async fn get_cask(name: &str) -> Result<Cask> {
229    let raw_json_result = fetch_cask(name).await;
230    let raw_json = match raw_json_result {
231        Ok(json_val) => json_val,
232        Err(e) => {
233            error!("Failed to fetch raw JSON for cask {}: {}", name, e);
234            return Err(e);
235        }
236    };
237    match serde_json::from_value::<Cask>(raw_json.clone()) {
238        Ok(cask) => Ok(cask),
239        Err(e) => {
240            error!("Failed to parse cask {} JSON: {}", name, e);
241            match serde_json::to_string_pretty(&raw_json) {
242                Ok(json_str) => {
243                    tracing::debug!("Problematic JSON for cask '{}':\n{}", name, json_str);
244                }
245                Err(fmt_err) => {
246                    tracing::debug!(
247                        "Could not pretty-print problematic JSON for cask {}: {}",
248                        name,
249                        fmt_err
250                    );
251                    tracing::debug!("Raw problematic value: {:?}", raw_json);
252                }
253            }
254            Err(SpsError::Json(Arc::new(e)))
255        }
256    }
257}
258
259pub async fn get_all_casks() -> Result<CaskList> {
260    let raw_data = fetch_all_casks().await?;
261    let casks: Vec<Cask> = serde_json::from_str(&raw_data).map_err(|e| {
262        error!("Failed to parse all_casks response: {}", e);
263        SpsError::Json(Arc::new(e))
264    })?;
265    Ok(CaskList { casks })
266}