hyperdb_api_core/client/
config.rs1use std::time::Duration;
7
8#[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 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 options: vec![("result_format_code".to_string(), "HyperBinary".to_string())],
57 }
58 }
59
60 pub fn with_host(mut self, host: impl Into<String>) -> Self {
62 self.host = host.into();
63 self
64 }
65
66 pub fn with_port(mut self, port: u16) -> Self {
68 self.port = port;
69 self
70 }
71
72 pub fn with_database(mut self, database: impl Into<String>) -> Self {
74 self.database = Some(database.into());
75 self
76 }
77
78 pub fn with_user(mut self, user: impl Into<String>) -> Self {
80 self.user = Some(user.into());
81 self
82 }
83
84 pub fn with_password(mut self, password: impl Into<String>) -> Self {
86 self.password = Some(password.into());
87 self
88 }
89
90 pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
92 self.connect_timeout = Some(timeout);
93 self
94 }
95
96 pub fn with_application_name(mut self, name: impl Into<String>) -> Self {
98 self.application_name = Some(name.into());
99 self
100 }
101
102 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 #[must_use]
110 pub fn host(&self) -> &str {
111 &self.host
112 }
113
114 #[must_use]
116 pub fn port(&self) -> u16 {
117 self.port
118 }
119
120 #[must_use]
122 pub fn database(&self) -> Option<&str> {
123 self.database.as_deref()
124 }
125
126 #[must_use]
128 pub fn user(&self) -> Option<&str> {
129 self.user.as_deref()
130 }
131
132 #[must_use]
134 pub fn password(&self) -> Option<&str> {
135 self.password.as_deref()
136 }
137
138 #[must_use]
140 pub fn connect_timeout(&self) -> Option<Duration> {
141 self.connect_timeout
142 }
143
144 #[must_use]
146 pub fn application_name(&self) -> Option<&str> {
147 self.application_name.as_deref()
148 }
149
150 #[must_use]
152 pub fn options(&self) -> &[(String, String)] {
153 &self.options
154 }
155
156 #[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 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 fn from_str(s: &str) -> Result<Self, Self::Err> {
189 let mut config = Config::new();
190
191 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 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 for param in query.split('&') {
218 if param.is_empty() {
219 continue;
220 }
221 if let Some(idx) = param.find('=') {
222 let name = ¶m[..idx];
223 let value = ¶m[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 #[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}