use anyhow::{Context, Result, anyhow};
use serde::{Serialize, de::DeserializeOwned};
use std::time::Duration;
use rsclaw_config as config;
const DEFAULT_PORT: u16 = 18888;
#[derive(Debug, Clone)]
pub struct GatewayEndpoint {
pub port: u16,
pub token: Option<String>,
}
impl GatewayEndpoint {
pub fn resolve() -> Self {
let cfg = config::load().ok();
let port = cfg.as_ref().map_or(DEFAULT_PORT, |c| c.gateway.port);
let token = cfg.as_ref().and_then(|c| c.gateway.auth_token.clone());
Self { port, token }
}
pub fn url(&self, path: &str) -> String {
format!("http://127.0.0.1:{}{}", self.port, path)
}
}
fn client(timeout: Duration) -> reqwest::Client {
reqwest::Client::builder()
.timeout(timeout)
.connect_timeout(Duration::from_secs(2))
.build()
.expect("reqwest client build")
}
fn auth_header<'a>(rb: reqwest::RequestBuilder, token: Option<&'a str>) -> reqwest::RequestBuilder {
match token {
Some(t) if !t.is_empty() => rb.header("Authorization", format!("Bearer {t}")),
_ => rb,
}
}
pub async fn is_gateway_up() -> bool {
let ep = GatewayEndpoint::resolve();
let c = client(Duration::from_millis(800));
matches!(
c.get(ep.url("/api/v1/health")).send().await,
Ok(r) if r.status().is_success()
)
}
pub async fn get_json<T: DeserializeOwned>(path: &str) -> Result<T> {
let ep = GatewayEndpoint::resolve();
let c = client(Duration::from_secs(30));
let rb = c.get(ep.url(path));
let resp = auth_header(rb, ep.token.as_deref())
.send()
.await
.with_context(|| format!("gateway GET {path} failed"))?;
if !resp.status().is_success() {
let st = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(anyhow!("gateway returned {st}: {}", body.chars().take(400).collect::<String>()));
}
resp.json::<T>()
.await
.with_context(|| format!("gateway GET {path}: invalid JSON response"))
}
pub async fn post_json<B: Serialize, T: DeserializeOwned>(path: &str, body: &B) -> Result<T> {
let ep = GatewayEndpoint::resolve();
let c = client(Duration::from_secs(60));
let rb = c.post(ep.url(path)).json(body);
let resp = auth_header(rb, ep.token.as_deref())
.send()
.await
.with_context(|| format!("gateway POST {path} failed"))?;
if !resp.status().is_success() {
let st = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(anyhow!("gateway returned {st}: {}", body.chars().take(400).collect::<String>()));
}
resp.json::<T>()
.await
.with_context(|| format!("gateway POST {path}: invalid JSON response"))
}
pub async fn patch_json<B: Serialize, T: DeserializeOwned>(path: &str, body: &B) -> Result<T> {
let ep = GatewayEndpoint::resolve();
let c = client(Duration::from_secs(30));
let rb = c.patch(ep.url(path)).json(body);
let resp = auth_header(rb, ep.token.as_deref())
.send()
.await
.with_context(|| format!("gateway PATCH {path} failed"))?;
if !resp.status().is_success() {
let st = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(anyhow!("gateway returned {st}: {}", body.chars().take(400).collect::<String>()));
}
resp.json::<T>()
.await
.with_context(|| format!("gateway PATCH {path}: invalid JSON response"))
}
pub async fn delete_json<T: DeserializeOwned>(path: &str) -> Result<T> {
let ep = GatewayEndpoint::resolve();
let c = client(Duration::from_secs(30));
let rb = c.delete(ep.url(path));
let resp = auth_header(rb, ep.token.as_deref())
.send()
.await
.with_context(|| format!("gateway DELETE {path} failed"))?;
if !resp.status().is_success() {
let st = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(anyhow!("gateway returned {st}: {}", body.chars().take(400).collect::<String>()));
}
resp.json::<T>()
.await
.with_context(|| format!("gateway DELETE {path}: invalid JSON response"))
}
pub async fn get_bytes(path: &str) -> Result<Vec<u8>> {
let ep = GatewayEndpoint::resolve();
let c = client(Duration::from_secs(60));
let rb = c.get(ep.url(path));
let resp = auth_header(rb, ep.token.as_deref())
.send()
.await
.with_context(|| format!("gateway GET {path} failed"))?;
if !resp.status().is_success() {
let st = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(anyhow!("gateway returned {st}: {}", body.chars().take(400).collect::<String>()));
}
Ok(resp.bytes().await?.to_vec())
}
pub fn down_hint() -> String {
"gateway is not reachable on loopback. start it with: rsclaw gateway start".to_string()
}