Skip to content

Commit d203718

Browse files
authored
Add new api module and debug printing
1 parent cfddcff commit d203718

File tree

14 files changed

+644
-1327
lines changed

14 files changed

+644
-1327
lines changed

src/api.rs

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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() > 4 {
43+
format!("Bearer ...{}", &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 with query parameters, returns parsed response.
70+
/// Parameters with `None` values are omitted.
71+
pub fn get_with_params<T: DeserializeOwned>(&self, path: &str, params: &[(&str, Option<String>)]) -> T {
72+
let filtered: Vec<(&str, &String)> = params.iter()
73+
.filter_map(|(k, v)| v.as_ref().map(|val| (*k, val)))
74+
.collect();
75+
let url = format!("{}{path}", self.api_url);
76+
self.log_request("GET", &url, None);
77+
78+
let resp = match self.build_request(reqwest::Method::GET, &url).query(&filtered).send() {
79+
Ok(r) => r,
80+
Err(e) => {
81+
eprintln!("error connecting to API: {e}");
82+
std::process::exit(1);
83+
}
84+
};
85+
86+
let (status, body) = util::debug_response(resp);
87+
if !status.is_success() {
88+
eprintln!("{}", util::api_error(body).red());
89+
std::process::exit(1);
90+
}
91+
92+
match serde_json::from_str(&body) {
93+
Ok(v) => v,
94+
Err(e) => {
95+
eprintln!("error parsing response: {e}");
96+
std::process::exit(1);
97+
}
98+
}
99+
}
100+
101+
/// GET request, returns parsed response.
102+
pub fn get<T: DeserializeOwned>(&self, path: &str) -> T {
103+
let url = format!("{}{path}", self.api_url);
104+
self.log_request("GET", &url, None);
105+
106+
let resp = match self.build_request(reqwest::Method::GET, &url).send() {
107+
Ok(r) => r,
108+
Err(e) => {
109+
eprintln!("error connecting to API: {e}");
110+
std::process::exit(1);
111+
}
112+
};
113+
114+
let (status, body) = util::debug_response(resp);
115+
if !status.is_success() {
116+
eprintln!("{}", util::api_error(body).red());
117+
std::process::exit(1);
118+
}
119+
120+
match serde_json::from_str(&body) {
121+
Ok(v) => v,
122+
Err(e) => {
123+
eprintln!("error parsing response: {e}");
124+
std::process::exit(1);
125+
}
126+
}
127+
}
128+
129+
/// POST request with JSON body, returns parsed response.
130+
pub fn post<T: DeserializeOwned>(&self, path: &str, body: &serde_json::Value) -> T {
131+
let url = format!("{}{path}", self.api_url);
132+
self.log_request("POST", &url, Some(body));
133+
134+
let resp = match self.build_request(reqwest::Method::POST, &url)
135+
.json(body)
136+
.send()
137+
{
138+
Ok(r) => r,
139+
Err(e) => {
140+
eprintln!("error connecting to API: {e}");
141+
std::process::exit(1);
142+
}
143+
};
144+
145+
let (status, resp_body) = util::debug_response(resp);
146+
if !status.is_success() {
147+
eprintln!("{}", util::api_error(resp_body).red());
148+
std::process::exit(1);
149+
}
150+
151+
match serde_json::from_str(&resp_body) {
152+
Ok(v) => v,
153+
Err(e) => {
154+
eprintln!("error parsing response: {e}");
155+
std::process::exit(1);
156+
}
157+
}
158+
}
159+
160+
/// POST request with JSON body, exits on error, returns raw (status, body).
161+
pub fn post_raw(&self, path: &str, body: &serde_json::Value) -> (reqwest::StatusCode, String) {
162+
let url = format!("{}{path}", self.api_url);
163+
self.log_request("POST", &url, Some(body));
164+
165+
let resp = match self.build_request(reqwest::Method::POST, &url)
166+
.json(body)
167+
.send()
168+
{
169+
Ok(r) => r,
170+
Err(e) => {
171+
eprintln!("error connecting to API: {e}");
172+
std::process::exit(1);
173+
}
174+
};
175+
176+
util::debug_response(resp)
177+
}
178+
179+
/// POST request with no body (e.g. execute endpoints), returns parsed response.
180+
pub fn post_empty<T: DeserializeOwned>(&self, path: &str) -> T {
181+
let url = format!("{}{path}", self.api_url);
182+
self.log_request("POST", &url, None);
183+
184+
let resp = match self.build_request(reqwest::Method::POST, &url).send() {
185+
Ok(r) => r,
186+
Err(e) => {
187+
eprintln!("error connecting to API: {e}");
188+
std::process::exit(1);
189+
}
190+
};
191+
192+
let (status, resp_body) = util::debug_response(resp);
193+
if !status.is_success() {
194+
eprintln!("{}", util::api_error(resp_body).red());
195+
std::process::exit(1);
196+
}
197+
198+
match serde_json::from_str(&resp_body) {
199+
Ok(v) => v,
200+
Err(e) => {
201+
eprintln!("error parsing response: {e}");
202+
std::process::exit(1);
203+
}
204+
}
205+
}
206+
207+
/// PUT request with JSON body, returns parsed response.
208+
pub fn put<T: DeserializeOwned>(&self, path: &str, body: &serde_json::Value) -> T {
209+
let url = format!("{}{path}", self.api_url);
210+
self.log_request("PUT", &url, Some(body));
211+
212+
let resp = match self.build_request(reqwest::Method::PUT, &url)
213+
.json(body)
214+
.send()
215+
{
216+
Ok(r) => r,
217+
Err(e) => {
218+
eprintln!("error connecting to API: {e}");
219+
std::process::exit(1);
220+
}
221+
};
222+
223+
let (status, resp_body) = util::debug_response(resp);
224+
if !status.is_success() {
225+
eprintln!("{}", util::api_error(resp_body).red());
226+
std::process::exit(1);
227+
}
228+
229+
match serde_json::from_str(&resp_body) {
230+
Ok(v) => v,
231+
Err(e) => {
232+
eprintln!("error parsing response: {e}");
233+
std::process::exit(1);
234+
}
235+
}
236+
}
237+
238+
/// POST with a custom request body (for file uploads). Returns raw status and body.
239+
pub fn post_body<R: std::io::Read + Send + 'static>(
240+
&self,
241+
path: &str,
242+
content_type: &str,
243+
reader: R,
244+
content_length: Option<u64>,
245+
) -> (reqwest::StatusCode, String) {
246+
let url = format!("{}{path}", self.api_url);
247+
self.log_request("POST", &url, None);
248+
249+
let mut req = self.build_request(reqwest::Method::POST, &url)
250+
.header("Content-Type", content_type);
251+
252+
if let Some(len) = content_length {
253+
req = req.header("Content-Length", len);
254+
}
255+
256+
let resp = match req.body(reqwest::blocking::Body::new(reader)).send() {
257+
Ok(r) => r,
258+
Err(e) => {
259+
eprintln!("error connecting to API: {e}");
260+
std::process::exit(1);
261+
}
262+
};
263+
264+
util::debug_response(resp)
265+
}
266+
267+
}

0 commit comments

Comments
 (0)