hyperdb_api_core/client/grpc/config.rs
1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! gRPC connection configuration.
5//!
6//! This module provides the [`GrpcConfig`] type for configuring gRPC connections
7//! to Hyper servers.
8
9use std::collections::HashMap;
10use std::time::Duration;
11
12use super::proto::hyper_service::query_param::TransferMode;
13
14/// Default maximum message size for gRPC requests/responses (64 MB).
15///
16/// This is larger than tonic's default (4 MB) to accommodate large query results
17/// when using `TransferMode::Sync`. For `Adaptive` or `Async` modes, results are
18/// streamed in smaller chunks, so this limit is less likely to be hit.
19pub const DEFAULT_MAX_MESSAGE_SIZE: usize = 64 * 1024 * 1024; // 64 MB
20
21/// Configuration for gRPC connections to Hyper servers.
22///
23/// # Example
24///
25/// ```
26/// use hyperdb_api_core::client::grpc::GrpcConfig;
27/// use std::time::Duration;
28///
29/// let config = GrpcConfig::new("http://localhost:7484")
30/// .database("my_database.hyper")
31/// .connect_timeout(Duration::from_secs(30))
32/// .request_timeout(Duration::from_secs(60));
33/// ```
34///
35/// # Message Size Limits
36///
37/// By default, the client uses a 64 MB message size limit. This is important when
38/// using `TransferMode::Sync`, which returns all results in a single response.
39/// For `TransferMode::Adaptive` (the default) or `TransferMode::Async`, results
40/// are streamed in chunks, making large message sizes less critical.
41///
42/// ```
43/// use hyperdb_api_core::client::grpc::{GrpcConfig, TransferMode};
44///
45/// // For very large SYNC results, increase the limit:
46/// let config = GrpcConfig::new("http://localhost:7484")
47/// .transfer_mode(TransferMode::Sync)
48/// .max_message_size(256 * 1024 * 1024); // 256 MB
49/// ```
50#[derive(Debug, Clone)]
51#[must_use = "GrpcConfig uses a consuming builder pattern - each method takes ownership and returns a new instance. You must use the returned value or your configuration changes will be lost"]
52pub struct GrpcConfig {
53 /// The endpoint URL (e.g., "<http://localhost:7484>" or "<https://hyper.example.com:443>")
54 pub(crate) endpoint: String,
55
56 /// Database path(s) to attach. Can be a single path or JSON array for multiple databases.
57 pub(crate) database: Option<String>,
58
59 /// Connection timeout
60 pub(crate) connect_timeout: Duration,
61
62 /// Request timeout (per-request)
63 pub(crate) request_timeout: Duration,
64
65 /// Transfer mode for query results
66 pub(crate) transfer_mode: TransferMode,
67
68 /// Use TLS (https) - automatically detected from endpoint URL
69 pub(crate) use_tls: bool,
70
71 /// Additional headers to send with requests (for authentication, routing, etc.)
72 pub(crate) headers: HashMap<String, String>,
73
74 /// Connection settings (passed as query parameters to Hyper)
75 pub(crate) settings: HashMap<String, String>,
76
77 /// Maximum size for decoding (receiving) gRPC messages in bytes.
78 /// Default is 64 MB. This is particularly important for `TransferMode::Sync`.
79 pub(crate) max_decoding_message_size: usize,
80
81 /// Maximum size for encoding (sending) gRPC messages in bytes.
82 /// Default is 64 MB.
83 pub(crate) max_encoding_message_size: usize,
84}
85
86impl GrpcConfig {
87 /// Creates a new gRPC configuration with the given endpoint.
88 ///
89 /// The endpoint should be a URL like `http://localhost:7484` or
90 /// `https://hyper-service.example.com:443`.
91 ///
92 /// # Example
93 ///
94 /// ```
95 /// use hyperdb_api_core::client::grpc::GrpcConfig;
96 ///
97 /// let config = GrpcConfig::new("http://localhost:7484");
98 /// ```
99 pub fn new(endpoint: impl Into<String>) -> Self {
100 let endpoint = endpoint.into();
101 let use_tls = endpoint.starts_with("https://");
102
103 GrpcConfig {
104 endpoint,
105 database: None,
106 connect_timeout: Duration::from_secs(30),
107 request_timeout: Duration::from_secs(100), // Match Hyper's default 100s timeout
108 transfer_mode: TransferMode::Adaptive, // Best default for most workloads
109 use_tls,
110 headers: HashMap::new(),
111 settings: HashMap::new(),
112 max_decoding_message_size: DEFAULT_MAX_MESSAGE_SIZE,
113 max_encoding_message_size: DEFAULT_MAX_MESSAGE_SIZE,
114 }
115 }
116
117 // Builder Methods (All now automatically protected by the struct-level #[must_use])
118
119 /// Sets the database path to attach.
120 ///
121 /// For a single database, provide the path directly. For multiple databases,
122 /// provide a JSON array like `[{"path": "db1.hyper", "alias": "db1"}, ...]`.
123 ///
124 /// # Example
125 ///
126 /// ```
127 /// use hyperdb_api_core::client::grpc::GrpcConfig;
128 ///
129 /// let config = GrpcConfig::new("http://localhost:7484")
130 /// .database("my_database.hyper");
131 /// ```
132 pub fn database(mut self, database: impl Into<String>) -> Self {
133 self.database = Some(database.into());
134 self
135 }
136
137 /// Sets the connection timeout.
138 ///
139 /// This is the maximum time to wait for the initial connection to be established.
140 /// Default is 30 seconds.
141 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
142 self.connect_timeout = timeout;
143 self
144 }
145
146 /// Sets the request timeout.
147 ///
148 /// This is the maximum time to wait for a single request to complete.
149 /// Default is 100 seconds (matching Hyper's server-side timeout for SYNC mode).
150 pub fn request_timeout(mut self, timeout: Duration) -> Self {
151 self.request_timeout = timeout;
152 self
153 }
154
155 /// Sets the transfer mode for query results.
156 ///
157 /// - `TransferMode::Sync` - All results in `ExecuteQuery` response (simple, 100s timeout)
158 /// - `TransferMode::Async` - Header only, fetch results via `GetQueryResult`
159 /// - `TransferMode::Adaptive` - First chunk inline, rest via `GetQueryResult` (default)
160 ///
161 /// `Adaptive` is recommended for most workloads as it provides the best balance
162 /// of latency and reliability.
163 pub fn transfer_mode(mut self, mode: TransferMode) -> Self {
164 self.transfer_mode = mode;
165 self
166 }
167
168 /// Sets the maximum message size for both encoding (sending) and decoding (receiving).
169 ///
170 /// This is a convenience method that sets both `max_decoding_message_size` and
171 /// `max_encoding_message_size` to the same value.
172 ///
173 /// Default is 64 MB. You may need to increase this when using `TransferMode::Sync`
174 /// with queries that return large result sets.
175 ///
176 /// # Example
177 ///
178 /// ```
179 /// use hyperdb_api_core::client::grpc::{GrpcConfig, TransferMode};
180 ///
181 /// // Allow up to 256 MB messages for large SYNC queries
182 /// let config = GrpcConfig::new("http://localhost:7484")
183 /// .transfer_mode(TransferMode::Sync)
184 /// .max_message_size(256 * 1024 * 1024);
185 /// ```
186 pub fn max_message_size(mut self, size: usize) -> Self {
187 self.max_decoding_message_size = size;
188 self.max_encoding_message_size = size;
189 self
190 }
191
192 /// Sets the maximum size for decoding (receiving) gRPC messages.
193 ///
194 /// Default is 64 MB. This is particularly important when using `TransferMode::Sync`,
195 /// which returns all query results in a single response message. If your queries
196 /// return more data than this limit, you will receive a "message too large" error.
197 ///
198 /// For `TransferMode::Adaptive` (default) or `TransferMode::Async`, results are
199 /// streamed in smaller chunks, making this limit less critical.
200 ///
201 /// # Example
202 ///
203 /// ```
204 /// use hyperdb_api_core::client::grpc::GrpcConfig;
205 ///
206 /// let config = GrpcConfig::new("http://localhost:7484")
207 /// .max_decoding_message_size(128 * 1024 * 1024); // 128 MB
208 /// ```
209 pub fn max_decoding_message_size(mut self, size: usize) -> Self {
210 self.max_decoding_message_size = size;
211 self
212 }
213
214 /// Sets the maximum size for encoding (sending) gRPC messages.
215 ///
216 /// Default is 64 MB. This affects the size of requests sent to the server,
217 /// which is typically small for queries but may be larger for parameterized
218 /// queries with large parameter payloads.
219 pub fn max_encoding_message_size(mut self, size: usize) -> Self {
220 self.max_encoding_message_size = size;
221 self
222 }
223
224 /// Adds a header to send with all requests.
225 ///
226 /// This is useful for authentication tokens, routing hints, or other metadata.
227 ///
228 /// # Example
229 ///
230 /// ```
231 /// use hyperdb_api_core::client::grpc::GrpcConfig;
232 ///
233 /// let config = GrpcConfig::new("https://hyper.example.com")
234 /// .header("authorization", "Bearer my-token")
235 /// .header("x-tenant-id", "tenant-123");
236 /// ```
237 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
238 self.headers.insert(key.into(), value.into());
239 self
240 }
241
242 /// Adds multiple headers at once.
243 pub fn headers(mut self, headers: impl IntoIterator<Item = (String, String)>) -> Self {
244 self.headers.extend(headers);
245 self
246 }
247
248 /// Adds a connection setting.
249 ///
250 /// These settings are passed to Hyper as query parameters.
251 /// See Hyper documentation for available settings.
252 ///
253 /// # Example
254 ///
255 /// ```
256 /// use hyperdb_api_core::client::grpc::GrpcConfig;
257 ///
258 /// let config = GrpcConfig::new("http://localhost:7484")
259 /// .setting("log_level", "debug");
260 /// ```
261 pub fn setting(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
262 self.settings.insert(key.into(), value.into());
263 self
264 }
265
266 /// Returns the endpoint URL.
267 #[must_use]
268 pub fn endpoint(&self) -> &str {
269 &self.endpoint
270 }
271
272 /// Returns the database path, if set.
273 #[must_use]
274 pub fn database_path(&self) -> Option<&str> {
275 self.database.as_deref()
276 }
277
278 /// Returns whether TLS is enabled.
279 #[must_use]
280 pub fn is_tls(&self) -> bool {
281 self.use_tls
282 }
283
284 /// Returns the maximum decoding message size.
285 #[must_use]
286 pub fn get_max_decoding_message_size(&self) -> usize {
287 self.max_decoding_message_size
288 }
289
290 /// Returns the maximum encoding message size.
291 #[must_use]
292 pub fn get_max_encoding_message_size(&self) -> usize {
293 self.max_encoding_message_size
294 }
295
296 /// Configures authentication headers from a DC JWT.
297 ///
298 /// Sets the `Authorization` and `audience` gRPC headers from the DC JWT.
299 ///
300 /// # Example
301 ///
302 /// ```no_run
303 /// use hyperdb_api_core::client::grpc::GrpcConfig;
304 /// use hyperdb_api_salesforce::{SalesforceAuthConfig, AuthMode, DataCloudTokenProvider};
305 ///
306 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
307 /// # let config = SalesforceAuthConfig::new("https://login.salesforce.com", "client_id")?
308 /// # .auth_mode(AuthMode::password("user", "pass"));
309 /// // Get a DC JWT
310 /// let mut provider = DataCloudTokenProvider::new(config)?;
311 /// let dc_jwt = provider.get_token().await?;
312 ///
313 /// // Configure gRPC client with the DC JWT
314 /// let grpc_config = GrpcConfig::new("https://hyper.data.salesforce.com")
315 /// .with_data_cloud_token(&dc_jwt);
316 /// # Ok(())
317 /// # }
318 /// ```
319 #[cfg(feature = "salesforce-auth")]
320 pub fn with_data_cloud_token(self, token: &hyperdb_api_salesforce::DataCloudToken) -> Self {
321 self.header("Authorization", token.bearer_token())
322 .header("audience", token.tenant_url_str())
323 }
324
325 /// Configures authentication headers with bearer token and audience.
326 ///
327 /// This is a lower-level method for manually setting authentication headers.
328 ///
329 /// # Arguments
330 ///
331 /// * `bearer_token` - The full Authorization header value (e.g., "Bearer abc123...")
332 /// * `audience` - The tenant URL or audience value
333 pub fn with_bearer_auth(
334 self,
335 bearer_token: impl Into<String>,
336 audience: impl Into<String>,
337 ) -> Self {
338 self.header("Authorization", bearer_token)
339 .header("audience", audience)
340 }
341}
342
343impl Default for GrpcConfig {
344 fn default() -> Self {
345 GrpcConfig::new("http://localhost:7484")
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn test_config_builder() {
355 let config = GrpcConfig::new("http://localhost:7484")
356 .database("test.hyper")
357 .connect_timeout(Duration::from_secs(10))
358 .request_timeout(Duration::from_secs(30))
359 .header("x-custom", "value")
360 .setting("log_level", "debug");
361
362 assert_eq!(config.endpoint, "http://localhost:7484");
363 assert_eq!(config.database, Some("test.hyper".to_string()));
364 assert_eq!(config.connect_timeout, Duration::from_secs(10));
365 assert_eq!(config.request_timeout, Duration::from_secs(30));
366 assert!(!config.use_tls);
367 assert_eq!(config.headers.get("x-custom"), Some(&"value".to_string()));
368 assert_eq!(config.settings.get("log_level"), Some(&"debug".to_string()));
369 }
370
371 #[expect(
372 clippy::similar_names,
373 reason = "paired bindings (request/response, reader/writer, etc.) are more readable with symmetric names than artificially distinct ones"
374 )]
375 #[test]
376 fn test_tls_detection() {
377 let http_config = GrpcConfig::new("http://localhost:7484");
378 assert!(!http_config.use_tls);
379
380 let https_config = GrpcConfig::new("https://hyper.example.com:443");
381 assert!(https_config.use_tls);
382 }
383
384 #[test]
385 fn test_default_values() {
386 let config = GrpcConfig::default();
387 assert_eq!(config.endpoint, "http://localhost:7484");
388 assert_eq!(config.connect_timeout, Duration::from_secs(30));
389 assert_eq!(config.request_timeout, Duration::from_secs(100));
390 assert!(matches!(config.transfer_mode, TransferMode::Adaptive));
391 assert_eq!(config.max_decoding_message_size, DEFAULT_MAX_MESSAGE_SIZE);
392 assert_eq!(config.max_encoding_message_size, DEFAULT_MAX_MESSAGE_SIZE);
393 }
394
395 #[test]
396 fn test_message_size_configuration() {
397 // Test max_message_size sets both
398 let config = GrpcConfig::new("http://localhost:7484").max_message_size(128 * 1024 * 1024);
399 assert_eq!(config.max_decoding_message_size, 128 * 1024 * 1024);
400 assert_eq!(config.max_encoding_message_size, 128 * 1024 * 1024);
401
402 // Test individual setters
403 let config = GrpcConfig::new("http://localhost:7484")
404 .max_decoding_message_size(256 * 1024 * 1024)
405 .max_encoding_message_size(32 * 1024 * 1024);
406 assert_eq!(config.max_decoding_message_size, 256 * 1024 * 1024);
407 assert_eq!(config.max_encoding_message_size, 32 * 1024 * 1024);
408
409 // Test getters
410 assert_eq!(config.get_max_decoding_message_size(), 256 * 1024 * 1024);
411 assert_eq!(config.get_max_encoding_message_size(), 32 * 1024 * 1024);
412 }
413
414 #[test]
415 fn test_sync_mode_with_large_message_size() {
416 // Common pattern: SYNC mode with increased message size for large results
417 let config = GrpcConfig::new("http://localhost:7484")
418 .transfer_mode(TransferMode::Sync)
419 .max_message_size(256 * 1024 * 1024);
420
421 assert!(matches!(config.transfer_mode, TransferMode::Sync));
422 assert_eq!(config.max_decoding_message_size, 256 * 1024 * 1024);
423 }
424}