1use serde::de::DeserializeOwned;
2use serde::Serialize;
3
4use crate::config::default_api_base;
5
6#[derive(Debug, serde::Deserialize)]
7struct RawEnvelope {
8 ok: bool,
9 data: Option<serde_json::Value>,
10 #[serde(default)]
11 error: Option<ApiErrorBody>,
12}
13
14#[derive(Debug, serde::Deserialize)]
15struct ApiErrorBody {
16 #[allow(dead_code)]
17 pub code: String,
18 pub message: String,
19}
20
21pub struct ControlPlaneClient {
22 base: String,
23 http: reqwest::Client,
24}
25
26impl ControlPlaneClient {
27 pub fn new(base: impl Into<String>) -> Self {
28 Self {
29 base: base.into().trim_end_matches('/').to_string(),
30 http: reqwest::Client::new(),
31 }
32 }
33
34 #[allow(dead_code)]
35 pub fn from_env() -> Self {
36 Self::new(default_api_base())
37 }
38
39 pub async fn post<T: DeserializeOwned, B: Serialize>(
40 &self,
41 path: &str,
42 body: &B,
43 bearer: Option<&str>,
44 ) -> anyhow::Result<T> {
45 let mut req = self.http.post(format!("{}{path}", self.base)).json(body);
46 if let Some(token) = bearer {
47 req = req.bearer_auth(token);
48 }
49 let response = req.send().await?;
50 self.decode(response).await
51 }
52
53 pub async fn get<T: DeserializeOwned>(&self, path: &str, bearer: &str) -> anyhow::Result<T> {
54 let response = self
55 .http
56 .get(format!("{}{path}", self.base))
57 .bearer_auth(bearer)
58 .send()
59 .await?;
60 self.decode(response).await
61 }
62
63 pub async fn delete<T: DeserializeOwned>(&self, path: &str, bearer: &str) -> anyhow::Result<T> {
64 let response = self
65 .http
66 .delete(format!("{}{path}", self.base))
67 .bearer_auth(bearer)
68 .send()
69 .await?;
70 self.decode(response).await
71 }
72
73 async fn decode<T: DeserializeOwned>(&self, response: reqwest::Response) -> anyhow::Result<T> {
74 let status = response.status();
75 let bytes = response.bytes().await?;
76 if bytes.is_empty() {
77 anyhow::bail!(
78 "{status} empty response from control plane — is flatland-control-plane up to date? (restart after pulling)"
79 );
80 }
81 let envelope: RawEnvelope = serde_json::from_slice(&bytes).map_err(|err| {
82 let preview = String::from_utf8_lossy(&bytes[..bytes.len().min(200)]);
83 anyhow::anyhow!("{status} invalid JSON from control plane: {err} (body: {preview})")
84 })?;
85 if !envelope.ok {
86 let message = envelope
87 .error
88 .map(|err| err.message)
89 .unwrap_or_else(|| "request failed".into());
90 anyhow::bail!("{status} {message}");
91 }
92 let data = envelope
93 .data
94 .ok_or_else(|| anyhow::anyhow!("{status} response missing data"))?;
95 Ok(serde_json::from_value(data)?)
96 }
97}