Skip to main content

smbcloud_gresiq_sdk/
client.rs

1use crate::client_credentials::GresiqCredentials;
2use crate::error::GresiqError;
3use serde::Serialize;
4use smbcloud_network::environment::Environment;
5use std::collections::HashMap;
6
7/// Talks to the smbCloud GresIQ REST gateway.
8///
9/// Cheap to clone — the inner `reqwest::Client` is `Arc`-backed.
10/// Build one at startup and clone it wherever you need it.
11///
12/// # Authentication
13///
14/// Every request carries two headers from the GresIQ credentials:
15/// `X-Gresiq-Api-Key` and `X-Gresiq-Api-Secret`. Get these from the
16/// GresIQ console after registering a database.
17///
18/// Additional headers can be layered on top via `with_extra_headers` —
19/// they ride alongside the GresIQ credentials on every subsequent request.
20#[derive(Debug, Clone)]
21pub struct GresiqClient {
22    base_url: String,
23    api_key: String,
24    api_secret: String,
25    extra_headers: HashMap<String, String>,
26    http: reqwest::Client,
27}
28
29impl GresiqClient {
30    /// Build a client from an environment and credentials.
31    ///
32    /// The base URL is resolved automatically from the environment:
33    /// - `Environment::Dev` → `http://localhost:8088`
34    /// - `Environment::Production` → `https://api.smbcloud.xyz`
35    pub fn from_credentials(environment: Environment, credentials: GresiqCredentials<'_>) -> Self {
36        let base_url = crate::client_credentials::base_url(&environment);
37        GresiqClient {
38            base_url,
39            api_key: credentials.api_key.to_string(),
40            api_secret: credentials.api_secret.to_string(),
41            extra_headers: HashMap::new(),
42            http: reqwest::Client::new(),
43        }
44    }
45
46    /// Attach additional headers sent on every request alongside the GresIQ
47    /// credentials. Replaces any previously set extra headers.
48    ///
49    /// Use this for secondary auth layers so the gateway can identify which
50    /// SDK client is writing, on top of which GresIQ app owns the database.
51    pub fn with_extra_headers(mut self, headers: HashMap<String, String>) -> Self {
52        self.extra_headers = headers;
53        self
54    }
55
56    /// POST a record into a GresIQ-managed table.
57    ///
58    /// `table` is the short, un-prefixed name from the REST path —
59    /// e.g. `"pulse/model_loaded"` or `"pulse_inference_events"`.
60    /// The gateway resolves the tenant prefix from the api_key.
61    ///
62    /// Returns `Err` on network failure or a non-2xx response. The caller
63    /// is responsible for deciding whether to retry, log, or ignore.
64    pub async fn insert<T: Serialize>(&self, table: &str, record: &T) -> Result<(), GresiqError> {
65        let url = format!("{}/gresiq/v1/{}", self.base_url, table);
66        let body = serde_json::json!({ "record": record });
67
68        let mut builder = self
69            .http
70            .post(&url)
71            .header("X-Gresiq-Api-Key", &self.api_key)
72            .header("X-Gresiq-Api-Secret", &self.api_secret)
73            .json(&body);
74
75        for (key, value) in &self.extra_headers {
76            builder = builder.header(key.as_str(), value.as_str());
77        }
78
79        let response = builder.send().await?;
80
81        if response.status().is_success() {
82            log::debug!("gresiq: {} inserted ok", table);
83            return Ok(());
84        }
85
86        let status = response.status().as_u16();
87        let message = response
88            .text()
89            .await
90            .unwrap_or_else(|_| "unreadable response body".to_string());
91
92        Err(GresiqError::Api { status, message })
93    }
94}