Skip to content

Commit d40be47

Browse files
committed
Add new api module and debug printing
1 parent cfddcff commit d40be47

14 files changed

Lines changed: 629 additions & 1324 deletions

src/api.rs

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
use crate::config;
2+
use crate::util;
3+
use crossterm::style::Stylize;
4+
use serde::de::DeserializeOwned;
5+
6+
pub struct ApiClient {
7+
client: reqwest::blocking::Client,
8+
api_key: String,
9+
pub api_url: String,
10+
workspace_id: Option<String>,
11+
}
12+
13+
impl ApiClient {
14+
/// Create a new API client. Loads config, validates auth.
15+
/// Pass `workspace_id` for endpoints that require it, or `None` for workspace-less endpoints.
16+
pub fn new(workspace_id: Option<&str>) -> Self {
17+
let profile_config = match config::load("default") {
18+
Ok(c) => c,
19+
Err(e) => {
20+
eprintln!("{e}");
21+
std::process::exit(1);
22+
}
23+
};
24+
25+
let api_key = match &profile_config.api_key {
26+
Some(key) if key != "PLACEHOLDER" => key.clone(),
27+
_ => {
28+
eprintln!("error: not authenticated. Run 'hotdata auth' to log in.");
29+
std::process::exit(1);
30+
}
31+
};
32+
33+
Self {
34+
client: reqwest::blocking::Client::new(),
35+
api_key,
36+
api_url: profile_config.api_url.to_string(),
37+
workspace_id: workspace_id.map(String::from),
38+
}
39+
}
40+
41+
fn debug_headers(&self) -> Vec<(&str, String)> {
42+
let masked = if self.api_key.len() > 8 {
43+
format!("Bearer {}...{}", &self.api_key[..4], &self.api_key[self.api_key.len()-4..])
44+
} else {
45+
"Bearer ***".to_string()
46+
};
47+
let mut headers = vec![("Authorization", masked)];
48+
if let Some(ref ws) = self.workspace_id {
49+
headers.push(("X-Workspace-Id", ws.clone()));
50+
}
51+
headers
52+
}
53+
54+
fn log_request(&self, method: &str, url: &str, body: Option<&serde_json::Value>) {
55+
let headers = self.debug_headers();
56+
let header_refs: Vec<(&str, &str)> = headers.iter().map(|(k, v)| (*k, v.as_str())).collect();
57+
util::debug_request(method, url, &header_refs, body);
58+
}
59+
60+
fn build_request(&self, method: reqwest::Method, url: &str) -> reqwest::blocking::RequestBuilder {
61+
let mut req = self.client.request(method, url)
62+
.header("Authorization", format!("Bearer {}", self.api_key));
63+
if let Some(ref ws) = self.workspace_id {
64+
req = req.header("X-Workspace-Id", ws);
65+
}
66+
req
67+
}
68+
69+
/// GET request, returns parsed response.
70+
pub fn get<T: DeserializeOwned>(&self, path: &str) -> T {
71+
let url = format!("{}{path}", self.api_url);
72+
self.log_request("GET", &url, None);
73+
74+
let resp = match self.build_request(reqwest::Method::GET, &url).send() {
75+
Ok(r) => r,
76+
Err(e) => {
77+
eprintln!("error connecting to API: {e}");
78+
std::process::exit(1);
79+
}
80+
};
81+
82+
let (status, body) = util::debug_response(resp);
83+
if !status.is_success() {
84+
eprintln!("{}", util::api_error(body).red());
85+
std::process::exit(1);
86+
}
87+
88+
match serde_json::from_str(&body) {
89+
Ok(v) => v,
90+
Err(e) => {
91+
eprintln!("error parsing response: {e}");
92+
std::process::exit(1);
93+
}
94+
}
95+
}
96+
97+
/// POST request with JSON body, returns parsed response.
98+
pub fn post<T: DeserializeOwned>(&self, path: &str, body: &serde_json::Value) -> T {
99+
let url = format!("{}{path}", self.api_url);
100+
self.log_request("POST", &url, Some(body));
101+
102+
let resp = match self.build_request(reqwest::Method::POST, &url)
103+
.json(body)
104+
.send()
105+
{
106+
Ok(r) => r,
107+
Err(e) => {
108+
eprintln!("error connecting to API: {e}");
109+
std::process::exit(1);
110+
}
111+
};
112+
113+
let (status, resp_body) = util::debug_response(resp);
114+
if !status.is_success() {
115+
eprintln!("{}", util::api_error(resp_body).red());
116+
std::process::exit(1);
117+
}
118+
119+
match serde_json::from_str(&resp_body) {
120+
Ok(v) => v,
121+
Err(e) => {
122+
eprintln!("error parsing response: {e}");
123+
std::process::exit(1);
124+
}
125+
}
126+
}
127+
128+
/// POST request with JSON body, exits on error, returns raw (status, body).
129+
pub fn post_raw(&self, path: &str, body: &serde_json::Value) -> (reqwest::StatusCode, String) {
130+
let url = format!("{}{path}", self.api_url);
131+
self.log_request("POST", &url, Some(body));
132+
133+
let resp = match self.build_request(reqwest::Method::POST, &url)
134+
.json(body)
135+
.send()
136+
{
137+
Ok(r) => r,
138+
Err(e) => {
139+
eprintln!("error connecting to API: {e}");
140+
std::process::exit(1);
141+
}
142+
};
143+
144+
util::debug_response(resp)
145+
}
146+
147+
/// POST request with no body (e.g. execute endpoints), returns parsed response.
148+
pub fn post_empty<T: DeserializeOwned>(&self, path: &str) -> T {
149+
let url = format!("{}{path}", self.api_url);
150+
self.log_request("POST", &url, None);
151+
152+
let resp = match self.build_request(reqwest::Method::POST, &url).send() {
153+
Ok(r) => r,
154+
Err(e) => {
155+
eprintln!("error connecting to API: {e}");
156+
std::process::exit(1);
157+
}
158+
};
159+
160+
let (status, resp_body) = util::debug_response(resp);
161+
if !status.is_success() {
162+
eprintln!("{}", util::api_error(resp_body).red());
163+
std::process::exit(1);
164+
}
165+
166+
match serde_json::from_str(&resp_body) {
167+
Ok(v) => v,
168+
Err(e) => {
169+
eprintln!("error parsing response: {e}");
170+
std::process::exit(1);
171+
}
172+
}
173+
}
174+
175+
/// PUT request with JSON body, returns parsed response.
176+
pub fn put<T: DeserializeOwned>(&self, path: &str, body: &serde_json::Value) -> T {
177+
let url = format!("{}{path}", self.api_url);
178+
self.log_request("PUT", &url, Some(body));
179+
180+
let resp = match self.build_request(reqwest::Method::PUT, &url)
181+
.json(body)
182+
.send()
183+
{
184+
Ok(r) => r,
185+
Err(e) => {
186+
eprintln!("error connecting to API: {e}");
187+
std::process::exit(1);
188+
}
189+
};
190+
191+
let (status, resp_body) = util::debug_response(resp);
192+
if !status.is_success() {
193+
eprintln!("{}", util::api_error(resp_body).red());
194+
std::process::exit(1);
195+
}
196+
197+
match serde_json::from_str(&resp_body) {
198+
Ok(v) => v,
199+
Err(e) => {
200+
eprintln!("error parsing response: {e}");
201+
std::process::exit(1);
202+
}
203+
}
204+
}
205+
206+
/// POST with a custom request body (for file uploads). Returns parsed response.
207+
pub fn post_body<T: DeserializeOwned, R: std::io::Read + Send + 'static>(
208+
&self,
209+
path: &str,
210+
content_type: &str,
211+
reader: R,
212+
content_length: Option<u64>,
213+
) -> (reqwest::StatusCode, String) {
214+
let url = format!("{}{path}", self.api_url);
215+
self.log_request("POST", &url, None);
216+
217+
let mut req = self.build_request(reqwest::Method::POST, &url)
218+
.header("Content-Type", content_type);
219+
220+
if let Some(len) = content_length {
221+
req = req.header("Content-Length", len);
222+
}
223+
224+
let resp = match req.body(reqwest::blocking::Body::new(reader)).send() {
225+
Ok(r) => r,
226+
Err(e) => {
227+
eprintln!("error connecting to API: {e}");
228+
std::process::exit(1);
229+
}
230+
};
231+
232+
util::debug_response(resp)
233+
}
234+
235+
/// Build a query string from optional parameters.
236+
pub fn query_string(params: &[(&str, Option<String>)]) -> String {
237+
let parts: Vec<String> = params.iter()
238+
.filter_map(|(k, v)| v.as_ref().map(|val| format!("{k}={val}")))
239+
.collect();
240+
if parts.is_empty() {
241+
String::new()
242+
} else {
243+
format!("?{}", parts.join("&"))
244+
}
245+
}
246+
}

0 commit comments

Comments
 (0)