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
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}