Skip to main content

burn_central_client/fleet/
mod.rs

1//! Fleet management client for Burn Central.
2
3pub mod request;
4pub mod response;
5
6use crate::ClientError;
7use crate::Env;
8use crate::client::ResponseExt;
9use crate::request::ExchangeFleetDeviceTokenRequest;
10use crate::response::FleetDeviceAuthTokenResponse;
11use request::{
12    DownloadModelRequest, IngestTelemetryRequest, SyncDeviceRequest, TelemetryIngestionEvents,
13};
14use reqwest::Url;
15use response::{FleetModelDownloadResponse, FleetSyncSnapshotResponse};
16
17/// A client for interacting with the Burn Central Fleet API.
18#[derive(Debug, Clone)]
19pub struct FleetClient {
20    http_client: reqwest::blocking::Client,
21    base_url: Url,
22}
23
24impl FleetClient {
25    /// Create a new FleetClient for the given environment.
26    pub fn new(env: Env) -> Self {
27        FleetClient {
28            http_client: reqwest::blocking::Client::new(),
29            base_url: env.get_url(),
30        }
31    }
32
33    /// Create a FleetClient with a custom base URL.
34    pub fn from_url(url: Url) -> Self {
35        FleetClient {
36            http_client: reqwest::blocking::Client::new(),
37            base_url: url,
38        }
39    }
40
41    /// Register the device and exchange credentials for a JWT.
42    pub fn register(
43        &self,
44        registration_token: impl Into<String>,
45        identity_key: impl Into<String>,
46        metadata: Option<serde_json::Value>,
47    ) -> Result<FleetDeviceAuthTokenResponse, ClientError> {
48        let request = ExchangeFleetDeviceTokenRequest {
49            registration_token: registration_token.into(),
50            identity_key: identity_key.into(),
51            metadata,
52        };
53
54        self.post_json("fleets/device/register", Some(request))
55    }
56
57    /// Sync device state with the fleet.
58    pub fn sync(
59        &self,
60        token: impl AsRef<str>,
61        metadata: Option<serde_json::Value>,
62    ) -> Result<FleetSyncSnapshotResponse, ClientError> {
63        let request = SyncDeviceRequest { metadata };
64
65        self.post_auth_json("fleets/device/sync", Some(request), token)
66    }
67
68    /// Get the model download information for the device's assigned fleet.
69    pub fn model_download(
70        &self,
71        auth_token: impl AsRef<str>,
72    ) -> Result<FleetModelDownloadResponse, ClientError> {
73        let request = DownloadModelRequest {};
74
75        self.post_auth_json("fleets/device/model/download", Some(request), auth_token)
76    }
77
78    /// Ingest telemetry events for a fleet device.
79    pub fn ingest_telemetry(
80        &self,
81        auth_token: impl AsRef<str>,
82        events: TelemetryIngestionEvents,
83    ) -> Result<(), ClientError> {
84        let request = IngestTelemetryRequest { events };
85
86        self.post_auth("fleets/device/telemetry", Some(request), auth_token)
87    }
88
89    fn post_auth<T>(
90        &self,
91        path: impl AsRef<str>,
92        body: Option<T>,
93        auth_token: impl AsRef<str>,
94    ) -> Result<(), ClientError>
95    where
96        T: serde::Serialize,
97    {
98        self.req(reqwest::Method::POST, path, body, Some(auth_token.as_ref()))
99            .map(|_| ())
100    }
101
102    fn post_json<T, R>(&self, path: impl AsRef<str>, body: Option<T>) -> Result<R, ClientError>
103    where
104        T: serde::Serialize,
105        R: for<'de> serde::Deserialize<'de>,
106    {
107        let response = self.req(reqwest::Method::POST, path, body, None)?;
108        let bytes = response.bytes()?;
109        let json = serde_json::from_slice::<R>(&bytes)?;
110        Ok(json)
111    }
112
113    fn post_auth_json<T, R>(
114        &self,
115        path: impl AsRef<str>,
116        body: Option<T>,
117        auth_token: impl AsRef<str>,
118    ) -> Result<R, ClientError>
119    where
120        T: serde::Serialize,
121        R: for<'de> serde::Deserialize<'de>,
122    {
123        let response = self.req(reqwest::Method::POST, path, body, Some(auth_token.as_ref()))?;
124        let bytes = response.bytes()?;
125        let json = serde_json::from_slice::<R>(&bytes)?;
126        Ok(json)
127    }
128
129    fn req<T: serde::Serialize>(
130        &self,
131        method: reqwest::Method,
132        path: impl AsRef<str>,
133        body: Option<T>,
134        auth_token: Option<&str>,
135    ) -> Result<reqwest::blocking::Response, ClientError> {
136        let url = self.join(path.as_ref());
137        let request_builder = self.http_client.request(method, url);
138
139        let mut request_builder = if let Some(body) = body {
140            request_builder
141                .body(serde_json::to_vec(&body)?)
142                .header(reqwest::header::CONTENT_TYPE, "application/json")
143        } else {
144            request_builder
145        };
146
147        if let Some(token) = auth_token {
148            request_builder = request_builder.bearer_auth(token);
149        }
150
151        let request_builder = request_builder.header("X-SDK-Version", env!("CARGO_PKG_VERSION"));
152
153        tracing::debug!("Sending fleet request: {:?}", request_builder);
154
155        let response = request_builder.send()?.map_to_burn_central_err()?;
156
157        tracing::debug!("Received fleet response: {:?}", response);
158
159        Ok(response)
160    }
161
162    fn join(&self, path: &str) -> Url {
163        self.join_versioned(path, 1)
164    }
165
166    fn join_versioned(&self, path: &str, version: u8) -> Url {
167        self.base_url
168            .join(&format!("v{version}/"))
169            .unwrap()
170            .join(path)
171            .expect("Should be able to join url")
172    }
173}