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}