@smbcloud / cli / commits / 2be0743

Refactor GresiqClient to use Environment and credentials struct

- Replace manual base URL and credential parsing with Environment and GresiqCredentials types from smbcloud-network - Add client_credentials module for credential struct and base_url helper - Update API to require explicit environment and credentials - Allow attaching extra headers to GresiqClient - Update docs and examples to match new API

keypair34 committed Apr 18, 2026 at 11:55 UTC 2be074337b090406a291c8ee713aaf22c2c3896e
8 files changed +92 -64
Cargo.lock
+1
@@ -2586,6 +2586,7 @@ dependencies = [
2586 "reqwest",
2587 "serde",
2588 "serde_json",
2589 + "smbcloud-network",
2590 "thiserror 2.0.18",
2591 "tokio",
2592 ]
Cargo.toml
+3 -4
@@ -13,7 +13,6 @@ crossterm = "0.28"
13 dialoguer = "0.11.0"
14 dirs = "6.0"
15
16 -
16 git2 = "0.20.1"
17 home = "0.5.4"
18 indicatif = "0.17.11"
@@ -27,13 +26,13 @@ reqwest = { version = "0.12.18", default-features = false }
26 serde = "1"
27 serde_json = "1.0.82"
28 serde_repr = "0.1"
29 +smbcloud-auth = { version = "0.3", path = "crates/smbcloud-auth" }
30 +smbcloud-auth-sdk = { version = "0.3", path = "crates/smbcloud-auth-sdk" }
31 +smbcloud-gresiq-sdk = { version = "0.1", path = "crates/smbcloud-gresiq-sdk" }
32 smbcloud-model = { version = "0.3", path = "crates/smbcloud-model" }
33 smbcloud-network = { version = "0.3", path = "crates/smbcloud-network" }
34 smbcloud-networking = { version = "0.3", path = "crates/smbcloud-networking" }
33 -smbcloud-auth = { version = "0.3", path = "crates/smbcloud-auth" }
34 -smbcloud-auth-sdk = { version = "0.3", path = "crates/smbcloud-auth-sdk" }
35 smbcloud-networking-project = { version = "0.3", path = "crates/smbcloud-networking-project" }
36 -smbcloud-gresiq-sdk = { version = "0.1", path = "crates/smbcloud-gresiq-sdk" }
36 smbcloud-utils = { version = "0.3", path = "crates/smbcloud-utils" }
37 spinners = "4.1.0"
38 strum = "0.27"
crates/cli/Cargo.toml
+1 -2
@@ -31,7 +31,6 @@ crossterm = { workspace = true }
31 dialoguer = { workspace = true }
32 dirs = { workspace = true }
33
34 -
34 git2 = { workspace = true }
35
36 home = { workspace = true }
@@ -44,10 +43,10 @@ reqwest = { workspace = true, features = ["json", "rustls-tls-native-roots"] }
43 serde = { workspace = true, features = ["derive"] }
44 serde_json = { workspace = true }
45 serde_repr = { workspace = true }
46 +smbcloud-auth = { workspace = true }
47 smbcloud-model = { workspace = true }
48 smbcloud-network = { workspace = true }
49 smbcloud-networking = { workspace = true }
50 -smbcloud-auth = { workspace = true }
50 smbcloud-networking-project = { workspace = true }
51 smbcloud-utils = { workspace = true }
52 spinners = { workspace = true }
crates/smbcloud-gresiq-sdk/Cargo.toml
+1
@@ -12,5 +12,6 @@ log = { workspace = true }
12 reqwest = { workspace = true, features = ["json", "rustls-tls"] }
13 serde = { workspace = true, features = ["derive"] }
14 serde_json = { workspace = true }
15 +smbcloud-network = { workspace = true }
16 thiserror = { workspace = true }
17 tokio = { workspace = true, features = ["rt"] }
crates/smbcloud-gresiq-sdk/src/client.rs
+44 -36
@@ -1,6 +1,8 @@
1 -use serde::Serialize;
2 -
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,40 +11,46 @@ use crate::error::GresiqError;
11 ///
12 /// # Authentication
13 ///
12 -/// Every request carries two headers set from the fields below:
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 {
17 - base_url: String,
18 - api_key: String,
22 + base_url: String,
23 + api_key: String,
24 api_secret: String,
20 - http: reqwest::Client,
25 + extra_headers: HashMap<String, String>,
26 + http: reqwest::Client,
27 }
28
29 impl GresiqClient {
24 - /// Build a client from explicit values.
25 - pub fn new(
26 - base_url: impl Into<String>,
27 - api_key: impl Into<String>,
28 - api_secret: impl Into<String>,
29 - ) -> Self {
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 {
31 - base_url: base_url.into(),
32 - api_key: api_key.into(),
33 - api_secret: api_secret.into(),
34 - http: reqwest::Client::new(),
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
38 - /// Read `GRESIQ_BASE_URL`, `GRESIQ_API_KEY`, and `GRESIQ_API_SECRET`
39 - /// from the environment. Returns `None` if any is missing — the caller
40 - /// decides what that means (skip telemetry, hard error, etc.).
41 - pub fn from_env() -> Option<Self> {
42 - let base_url = std::env::var("GRESIQ_BASE_URL").ok()?;
43 - let api_key = std::env::var("GRESIQ_API_KEY").ok()?;
44 - let api_secret = std::env::var("GRESIQ_API_SECRET").ok()?;
45 - Some(Self::new(base_url, api_key, api_secret))
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.
@@ -53,29 +61,29 @@ impl GresiqClient {
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.
56 - pub async fn insert<T: Serialize>(
57 - &self,
58 - table: &str,
59 - record: &T,
60 - ) -> Result<(), GresiqError> {
61 - let url = format!("{}/gresiq/v1/{}", self.base_url, table);
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
64 - let response = self
68 + let mut builder = self
69 .http
70 .post(&url)
67 - .header("X-Gresiq-Api-Key", &self.api_key)
71 + .header("X-Gresiq-Api-Key", &self.api_key)
72 .header("X-Gresiq-Api-Secret", &self.api_secret)
69 - .json(&body)
70 - .send()
71 - .await?;
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
78 - let status = response.status().as_u16();
86 + let status = response.status().as_u16();
87 let message = response
88 .text()
89 .await
crates/smbcloud-gresiq-sdk/src/client_credentials.rs new
+23
@@ -0,0 +1,23 @@
1 +use smbcloud_network::environment::Environment;
2 +
3 +/// API credentials for a GresIQ-managed database.
4 +///
5 +/// Obtain these from the smbCloud console after registering a GresIQ app.
6 +/// The `api_key` identifies the app; the `api_secret` authenticates it.
7 +#[derive(Clone, Copy)]
8 +pub struct GresiqCredentials<'a> {
9 + pub api_key: &'a str,
10 + pub api_secret: &'a str,
11 +}
12 +
13 +/// Resolve the GresIQ gateway base URL for the given environment.
14 +///
15 +/// - **Dev** → `http://localhost:8088`
16 +/// - **Production** → `https://api.smbcloud.xyz`
17 +pub fn base_url(environment: &Environment) -> String {
18 + format!(
19 + "{}://{}",
20 + environment.api_protocol(),
21 + environment.api_host()
22 + )
23 +}
crates/smbcloud-gresiq-sdk/src/smbcloud_gresiq_sdk.rs
+12 -5
@@ -5,13 +5,12 @@
5 //! interface for inserting and querying rows.
6 //!
7 //! This crate handles the HTTP transport and auth headers. Schema knowledge
8 -//! (which tables exist, what the rows look like) lives in the caller — see
9 -//! `onde::pulse` for an example of wrapping this for a specific schema.
8 +//! (which tables exist, what the rows look like) lives in the caller.
9 //!
10 //! # Quick start
11 //!
12 //! ```no_run
14 -//! use smbcloud_gresiq_sdk::GresiqClient;
13 +//! use smbcloud_gresiq_sdk::{Environment, GresiqClient, GresiqCredentials};
14 //! use serde::Serialize;
15 //!
16 //! #[derive(Serialize)]
@@ -19,8 +18,13 @@
18 //!
19 //! #[tokio::main]
20 //! async fn main() -> anyhow::Result<()> {
22 -//! let client = GresiqClient::from_env()
23 -//! .expect("set GRESIQ_BASE_URL, GRESIQ_API_KEY, GRESIQ_API_SECRET");
21 +//! let client = GresiqClient::from_credentials(
22 +//! Environment::Dev,
23 +//! GresiqCredentials {
24 +//! api_key: "your-key",
25 +//! api_secret: "your-secret",
26 +//! },
27 +//! );
28 //!
29 //! client.insert("hits", &Hit {
30 //! path: "/api/chat".into(),
@@ -32,7 +36,10 @@
36 //! ```
37
38 mod client;
39 +mod client_credentials;
40 mod error;
41
42 pub use client::GresiqClient;
43 +pub use client_credentials::{base_url, GresiqCredentials};
44 pub use error::GresiqError;
45 +pub use smbcloud_network::environment::Environment;
crates/smbcloud-gresiq/src/rest.rs
+7 -17
@@ -147,18 +147,12 @@ impl GresiqRestClient {
147 }
148
149 /// Attaches the standard request headers required by the GresIQ gateway.
150 - pub(crate) fn apply_headers(
151 - &self,
152 - req: reqwest::RequestBuilder,
153 - ) -> reqwest::RequestBuilder {
150 + pub(crate) fn apply_headers(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
151 let schema = self.config.schema.as_deref().unwrap_or("public");
155 - req.header(
156 - "Authorization",
157 - format!("Bearer {}", self.config.api_key),
158 - )
159 - .header("Content-Type", "application/json")
160 - .header("Accept-Profile", schema)
161 - .header("Content-Profile", schema)
152 + req.header("Authorization", format!("Bearer {}", self.config.api_key))
153 + .header("Content-Type", "application/json")
154 + .header("Accept-Profile", schema)
155 + .header("Content-Profile", schema)
156 }
157 }
158
@@ -371,12 +365,8 @@ impl<'c> QueryBuilder<'c> {
365 // the operation variant.
366 let (http_method, prefer_header, body) = match operation {
367 Operation::Select => ("GET", "return=representation".to_owned(), None),
374 - Operation::Insert(rows) => {
375 - ("POST", "return=representation".to_owned(), Some(rows))
376 - }
377 - Operation::Update(patch) => {
378 - ("PATCH", "return=representation".to_owned(), Some(patch))
379 - }
368 + Operation::Insert(rows) => ("POST", "return=representation".to_owned(), Some(rows)),
369 + Operation::Update(patch) => ("PATCH", "return=representation".to_owned(), Some(patch)),
370 Operation::Upsert(rows) => (
371 "POST",
372 "return=representation,resolution=merge-duplicates".to_owned(),