use std::collections::HashMap;
use std::io::IsTerminal;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::Client;
use serde_json::Value;
use super::error::ApiError;
pub struct ApiClient {
client: Client,
api_key: String,
base_url: String,
}
impl ApiClient {
pub fn new(api_key: String, base_url: String) -> Self {
let client = Client::builder()
.user_agent("zilliz-cli/1.0")
.timeout(std::time::Duration::from_secs(60))
.connect_timeout(std::time::Duration::from_secs(30))
.build()
.expect("Failed to build HTTP client");
Self {
client,
api_key,
base_url: base_url.trim_end_matches('/').to_string(),
}
}
pub async fn call(
&self,
method: &str,
path: &str,
path_params: Option<&HashMap<String, String>>,
body: Option<&Value>,
) -> Result<Value, ApiError> {
let mut resolved_path = path.to_string();
if let Some(params) = path_params {
for (key, value) in params {
let encoded = urlencoding::encode(value);
resolved_path = resolved_path.replace(&format!("{{{}}}", key), &encoded);
}
}
let url = format!("{}{}", self.base_url, resolved_path);
let request = match method.to_uppercase().as_str() {
"GET" => {
let mut req = self.client.get(&url);
if let Some(body) = body {
if let Some(obj) = body.as_object() {
let params: Vec<(String, String)> = obj
.iter()
.map(|(k, v)| {
let val = match v {
Value::String(s) => s.clone(),
other => other.to_string(),
};
(k.clone(), val)
})
.collect();
req = req.query(¶ms);
}
}
req
}
_ => {
let json_body = body.cloned().unwrap_or(Value::Object(Default::default()));
self.client
.request(method.parse().unwrap_or(reqwest::Method::POST), &url)
.json(&json_body)
}
};
let spinner = if std::io::stderr().is_terminal() {
let sp = ProgressBar::new_spinner().with_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.unwrap(),
);
sp.set_message("Loading...");
sp.enable_steady_tick(std::time::Duration::from_millis(80));
Some(sp)
} else {
None
};
let resp = request
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Accept", "application/json")
.send()
.await?;
if let Some(sp) = &spinner {
sp.finish_and_clear();
}
let status = resp.status();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
let text = resp.text().await?;
if !content_type.contains("json") && text.trim_start().starts_with('<') {
return Err(ApiError::non_json_response(status, &content_type));
}
let data: Value =
serde_json::from_str(&text).map_err(|_| ApiError::InvalidJson(text.clone()))?;
let obj = data
.as_object()
.ok_or_else(|| ApiError::InvalidJson(text))?;
let code = obj.get("code").and_then(|v| v.as_i64()).unwrap_or(0);
if (code == 0 || code == 200) && status.is_success() {
Ok(obj
.get("data")
.cloned()
.unwrap_or_else(|| Value::Object(obj.clone())))
} else {
let message = obj
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error")
.to_string();
Err(ApiError::api(code, message))
}
}
}