feature/smbcloud-deploy
claude/smbcloud-github-app-k9641p
development
feature/apps
feature/build-release
feature/ci-docs
feature/ci-ruby-gems
feature/cicd
feature/completion
feature/dist-nuget
feature/fix-ci-sdk-pypi-rubygem
feature/fix-clippy-on-stable-channel
feature/fix-deployment-script
feature/github-email
feature/gresiq-v1
feature/project-new-structure
feature/rsync-deploy
feature/ruby-runner
feature/sdk-npm
feature/sdk-workflows
feature/smbcloud-cli-release-npm
feature/smbcloud-deploy
feature/wasm
main
release/v0.7.4
| 1 | use crate::client_credentials::GresiqCredentials; |
| 2 | use crate::error::GresiqError; |
| 3 | use serde::Serialize; |
| 4 | use smbcloud_network::environment::Environment; |
| 5 | use 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)] |
| 21 | pub 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 | |
| 29 | impl 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 | } |