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
46 .http
47 .post(format!("{}{path}", self.base))
48 .json(body);
49 if let Some(token) = bearer {
50 req = req.bearer_auth(token);
51 }
52 let response = req.send().await?;
53 self.decode(response).await
54 }
55
56 pub async fn get<T: DeserializeOwned>(&self, path: &str, bearer: &str) -> anyhow::Result<T> {
57 let response = self
58 .http
59 .get(format!("{}{path}", self.base))
60 .bearer_auth(bearer)
61 .send()
62 .await?;
63 self.decode(response).await
64 }
65
66 pub async fn delete<T: DeserializeOwned>(&self, path: &str, bearer: &str) -> anyhow::Result<T> {
67 let response = self
68 .http
69 .delete(format!("{}{path}", self.base))
70 .bearer_auth(bearer)
71 .send()
72 .await?;
73 self.decode(response).await
74 }
75
76 async fn decode<T: DeserializeOwned>(
77 &self,
78 response: reqwest::Response,
79 ) -> anyhow::Result<T> {
80 let status = response.status();
81 let bytes = response.bytes().await?;
82 if bytes.is_empty() {
83 anyhow::bail!(
84 "{status} empty response from control plane — is flatland-control-plane up to date? (restart after pulling)"
85 );
86 }
87 let envelope: RawEnvelope = serde_json::from_slice(&bytes).map_err(|err| {
88 let preview = String::from_utf8_lossy(&bytes[..bytes.len().min(200)]);
89 anyhow::anyhow!("{status} invalid JSON from control plane: {err} (body: {preview})")
90 })?;
91 if !envelope.ok {
92 let message = envelope
93 .error
94 .map(|err| err.message)
95 .unwrap_or_else(|| "request failed".into());
96 anyhow::bail!("{status} {message}");
97 }
98 let data = envelope
99 .data
100 .ok_or_else(|| anyhow::anyhow!("{status} response missing data"))?;
101 Ok(serde_json::from_value(data)?)
102 }
103}