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}