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(),