use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::config::default_api_base;
#[derive(Debug, serde::Deserialize)]
struct RawEnvelope {
ok: bool,
data: Option<serde_json::Value>,
#[serde(default)]
error: Option<ApiErrorBody>,
}
#[derive(Debug, serde::Deserialize)]
struct ApiErrorBody {
#[allow(dead_code)]
pub code: String,
pub message: String,
}
pub struct ControlPlaneClient {
base: String,
http: reqwest::Client,
}
impl ControlPlaneClient {
pub fn new(base: impl Into<String>) -> Self {
Self {
base: base.into().trim_end_matches('/').to_string(),
http: reqwest::Client::new(),
}
}
#[allow(dead_code)]
pub fn from_env() -> Self {
Self::new(default_api_base())
}
pub async fn post<T: DeserializeOwned, B: Serialize>(
&self,
path: &str,
body: &B,
bearer: Option<&str>,
) -> anyhow::Result<T> {
let mut req = self
.http
.post(format!("{}{path}", self.base))
.json(body);
if let Some(token) = bearer {
req = req.bearer_auth(token);
}
let response = req.send().await?;
self.decode(response).await
}
pub async fn get<T: DeserializeOwned>(&self, path: &str, bearer: &str) -> anyhow::Result<T> {
let response = self
.http
.get(format!("{}{path}", self.base))
.bearer_auth(bearer)
.send()
.await?;
self.decode(response).await
}
pub async fn delete<T: DeserializeOwned>(&self, path: &str, bearer: &str) -> anyhow::Result<T> {
let response = self
.http
.delete(format!("{}{path}", self.base))
.bearer_auth(bearer)
.send()
.await?;
self.decode(response).await
}
async fn decode<T: DeserializeOwned>(
&self,
response: reqwest::Response,
) -> anyhow::Result<T> {
let status = response.status();
let bytes = response.bytes().await?;
if bytes.is_empty() {
anyhow::bail!(
"{status} empty response from control plane — is flatland-control-plane up to date? (restart after pulling)"
);
}
let envelope: RawEnvelope = serde_json::from_slice(&bytes).map_err(|err| {
let preview = String::from_utf8_lossy(&bytes[..bytes.len().min(200)]);
anyhow::anyhow!("{status} invalid JSON from control plane: {err} (body: {preview})")
})?;
if !envelope.ok {
let message = envelope
.error
.map(|err| err.message)
.unwrap_or_else(|| "request failed".into());
anyhow::bail!("{status} {message}");
}
let data = envelope
.data
.ok_or_else(|| anyhow::anyhow!("{status} response missing data"))?;
Ok(serde_json::from_value(data)?)
}
}