Skip to main content

flatland_cli/
api.rs

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}