use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use reqwest::{Method, StatusCode};
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::config::Config;
use crate::exit::{CtlError, CtlResult};
const USER_AGENT: &str = concat!("cellctl/", env!("CARGO_PKG_VERSION"));
#[derive(Clone)]
pub struct CellosClient {
base: String,
http: reqwest::Client,
}
impl CellosClient {
pub fn new(cfg: &Config) -> CtlResult<Self> {
let base = cfg.effective_server();
let base = base.trim_end_matches('/').to_string();
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
if let Some(tok) = cfg.effective_token() {
let v = HeaderValue::from_str(&format!("Bearer {tok}"))
.map_err(|e| CtlError::usage(format!("bad token: {e}")))?;
headers.insert(AUTHORIZATION, v);
}
let http = reqwest::Client::builder()
.user_agent(USER_AGENT)
.default_headers(headers)
.build()
.map_err(|e| CtlError::api(format!("init http client: {e}")))?;
Ok(Self { base, http })
}
pub fn base_url(&self) -> &str {
&self.base
}
fn url(&self, path: &str) -> String {
if path.starts_with('/') {
format!("{}{}", self.base, path)
} else {
format!("{}/{}", self.base, path)
}
}
pub async fn get_json<T: DeserializeOwned>(&self, path: &str) -> CtlResult<T> {
let resp = self.http.get(self.url(path)).send().await?;
decode_json(resp).await
}
pub async fn post_json<B: Serialize, T: DeserializeOwned>(
&self,
path: &str,
body: &B,
) -> CtlResult<T> {
let resp = self.http.post(self.url(path)).json(body).send().await?;
decode_json(resp).await
}
pub async fn delete(&self, path: &str) -> CtlResult<()> {
let resp = self.http.delete(self.url(path)).send().await?;
check_status(resp).await.map(|_| ())
}
pub async fn get_stream(&self, path: &str) -> CtlResult<reqwest::Response> {
let resp = self.http.get(self.url(path)).send().await?;
check_status(resp).await
}
pub fn ws_url(&self, path: &str) -> CtlResult<String> {
let mut u = url::Url::parse(&self.url(path))
.map_err(|e| CtlError::usage(format!("bad url: {e}")))?;
match u.scheme() {
"http" => u
.set_scheme("ws")
.map_err(|_| CtlError::usage("set ws scheme"))?,
"https" => u
.set_scheme("wss")
.map_err(|_| CtlError::usage("set wss scheme"))?,
"ws" | "wss" => {}
other => return Err(CtlError::usage(format!("unsupported scheme: {other}"))),
}
Ok(u.to_string())
}
#[allow(dead_code)]
pub fn auth_header(&self) -> Option<String> {
self.http
.request(Method::GET, &self.base)
.build()
.ok()
.and_then(|req| req.headers().get(AUTHORIZATION).cloned())
.and_then(|v| v.to_str().ok().map(|s| s.to_string()))
}
}
async fn check_status(resp: reqwest::Response) -> CtlResult<reqwest::Response> {
let status = resp.status();
if status.is_success() {
return Ok(resp);
}
let body = resp.text().await.unwrap_or_default();
let body_trim = body.trim();
let detail = if body_trim.is_empty() {
format!("server returned {status}")
} else {
format!("server returned {status}: {body_trim}")
};
Err(match status {
StatusCode::BAD_REQUEST | StatusCode::UNPROCESSABLE_ENTITY => CtlError::validation(detail),
_ => CtlError::api(detail),
})
}
async fn decode_json<T: DeserializeOwned>(resp: reqwest::Response) -> CtlResult<T> {
let resp = check_status(resp).await?;
let bytes = resp.bytes().await?;
serde_json::from_slice(&bytes).map_err(|e| CtlError::api(format!("decode json: {e}")))
}