Skip to main content

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}