Skip to main content

hyperdb_api_core/client/
config.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Connection configuration for Hyper client.
5
6use std::time::Duration;
7
8/// Configuration for a Hyper database connection.
9///
10/// Use the builder pattern to construct a configuration:
11///
12/// ```no_run
13/// // Marked `no_run` to dodge a Windows Defender heuristic that intermittently
14/// // refuses to launch this specific compiled doctest binary with
15/// // `ERROR_ACCESS_DENIED`. The same builder chain is exercised by
16/// // `tests::test_config_builder` so coverage is preserved.
17/// use hyperdb_api_core::client::Config;
18/// use std::time::Duration;
19///
20/// let config = Config::new()
21///     .with_host("localhost")
22///     .with_port(7483)
23///     .with_database("test.hyper")
24///     .with_user("myuser")
25///     .with_password("mypass")
26///     .with_connect_timeout(Duration::from_secs(30));
27/// ```
28#[derive(Debug, Clone)]
29#[must_use = "Config 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"]
30pub struct Config {
31    host: String,
32    port: u16,
33    database: Option<String>,
34    user: Option<String>,
35    password: Option<String>,
36    connect_timeout: Option<Duration>,
37    application_name: Option<String>,
38    options: Vec<(String, String)>,
39}
40
41impl Config {
42    /// Creates a new configuration with default settings.
43    ///
44    /// By default, this sets `result_format_code=HyperBinary` for optimal
45    /// performance with Hyper's native binary format.
46    pub fn new() -> Self {
47        Config {
48            host: "localhost".to_string(),
49            port: 7483,
50            database: None,
51            user: None,
52            password: None,
53            connect_timeout: Some(Duration::from_secs(30)),
54            application_name: None,
55            // Set HyperBinary format by default for optimal performance
56            options: vec![("result_format_code".to_string(), "HyperBinary".to_string())],
57        }
58    }
59
60    /// Sets the host.
61    pub fn with_host(mut self, host: impl Into<String>) -> Self {
62        self.host = host.into();
63        self
64    }
65
66    /// Sets the port.
67    pub fn with_port(mut self, port: u16) -> Self {
68        self.port = port;
69        self
70    }
71
72    /// Sets the database name.
73    pub fn with_database(mut self, database: impl Into<String>) -> Self {
74        self.database = Some(database.into());
75        self
76    }
77
78    /// Sets the username.
79    pub fn with_user(mut self, user: impl Into<String>) -> Self {
80        self.user = Some(user.into());
81        self
82    }
83
84    /// Sets the password.
85    pub fn with_password(mut self, password: impl Into<String>) -> Self {
86        self.password = Some(password.into());
87        self
88    }
89
90    /// Sets the connection timeout.
91    pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
92        self.connect_timeout = Some(timeout);
93        self
94    }
95
96    /// Sets the application name.
97    pub fn with_application_name(mut self, name: impl Into<String>) -> Self {
98        self.application_name = Some(name.into());
99        self
100    }
101
102    /// Adds a custom connection option.
103    pub fn with_option(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
104        self.options.push((name.into(), value.into()));
105        self
106    }
107
108    /// Returns the host.
109    #[must_use]
110    pub fn host(&self) -> &str {
111        &self.host
112    }
113
114    /// Returns the port.
115    #[must_use]
116    pub fn port(&self) -> u16 {
117        self.port
118    }
119
120    /// Returns the database name.
121    #[must_use]
122    pub fn database(&self) -> Option<&str> {
123        self.database.as_deref()
124    }
125
126    /// Returns the username.
127    #[must_use]
128    pub fn user(&self) -> Option<&str> {
129        self.user.as_deref()
130    }
131
132    /// Returns the password.
133    #[must_use]
134    pub fn password(&self) -> Option<&str> {
135        self.password.as_deref()
136    }
137
138    /// Returns the connection timeout.
139    #[must_use]
140    pub fn connect_timeout(&self) -> Option<Duration> {
141        self.connect_timeout
142    }
143
144    /// Returns the application name.
145    #[must_use]
146    pub fn application_name(&self) -> Option<&str> {
147        self.application_name.as_deref()
148    }
149
150    /// Returns the connection options.
151    #[must_use]
152    pub fn options(&self) -> &[(String, String)] {
153        &self.options
154    }
155
156    /// Returns the startup parameters for the connection.
157    #[must_use]
158    pub fn startup_params(&self) -> Vec<(&str, &str)> {
159        let mut params = Vec::new();
160
161        if let Some(ref user) = self.user {
162            params.push(("user", user.as_str()));
163        }
164
165        if let Some(ref database) = self.database {
166            params.push(("database", database.as_str()));
167        }
168
169        if let Some(ref app_name) = self.application_name {
170            params.push(("application_name", app_name.as_str()));
171        }
172
173        // Add custom options
174        for (name, value) in &self.options {
175            params.push((name.as_str(), value.as_str()));
176        }
177
178        params
179    }
180}
181
182impl std::str::FromStr for Config {
183    type Err = String;
184
185    /// Parses a connection string into a Config.
186    ///
187    /// Format: `host:port/database?user=xxx&password=xxx`
188    fn from_str(s: &str) -> Result<Self, Self::Err> {
189        let mut config = Config::new();
190
191        // Parse host:port
192        let (addr, rest) = if let Some(idx) = s.find('/') {
193            (&s[..idx], &s[idx + 1..])
194        } else {
195            (s, "")
196        };
197
198        if let Some(idx) = addr.rfind(':') {
199            config.host = addr[..idx].to_string();
200            config.port = addr[idx + 1..].parse().map_err(|_| "invalid port number")?;
201        } else {
202            config.host = addr.to_string();
203        }
204
205        // Parse database and query params
206        let (database, query) = if let Some(idx) = rest.find('?') {
207            (&rest[..idx], &rest[idx + 1..])
208        } else {
209            (rest, "")
210        };
211
212        if !database.is_empty() {
213            config.database = Some(database.to_string());
214        }
215
216        // Parse query parameters
217        for param in query.split('&') {
218            if param.is_empty() {
219                continue;
220            }
221            if let Some(idx) = param.find('=') {
222                let name = &param[..idx];
223                let value = &param[idx + 1..];
224                match name {
225                    "user" => config.user = Some(value.to_string()),
226                    "password" => config.password = Some(value.to_string()),
227                    "application_name" => config.application_name = Some(value.to_string()),
228                    _ => config.options.push((name.to_string(), value.to_string())),
229                }
230            }
231        }
232
233        Ok(config)
234    }
235}
236
237impl Default for Config {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_config_from_str() {
249        let config: Config = "localhost:7483/mydb?user=test".parse().unwrap();
250        assert_eq!(config.host, "localhost");
251        assert_eq!(config.port, 7483);
252        assert_eq!(config.database, Some("mydb".to_string()));
253        assert_eq!(config.user, Some("test".to_string()));
254    }
255
256    /// Mirrors the `///` example on `Config` so the builder chain is still
257    /// exercised at runtime even though the doctest itself is `no_run` (see
258    /// the doc comment above for why).
259    #[test]
260    fn test_config_builder() {
261        let config = Config::new()
262            .with_host("localhost")
263            .with_port(7483)
264            .with_database("test.hyper")
265            .with_user("myuser")
266            .with_password("mypass")
267            .with_connect_timeout(Duration::from_secs(30));
268
269        assert_eq!(config.host, "localhost");
270        assert_eq!(config.port, 7483);
271        assert_eq!(config.database.as_deref(), Some("test.hyper"));
272        assert_eq!(config.user.as_deref(), Some("myuser"));
273        assert_eq!(config.password.as_deref(), Some("mypass"));
274        assert_eq!(config.connect_timeout, Some(Duration::from_secs(30)));
275    }
276}