1#[derive(Debug, Clone)]
2pub struct ClientConfig {
3 pub host: String,
4 pub port: u16,
5 pub base_path: String,
6 pub tls: bool,
7 pub accept_invalid_certs: bool,
8 pub timeout_secs: u64,
9 pub proxy: Option<String>,
14 pub proxy_username: Option<String>,
15 pub proxy_password: Option<String>,
16}
17
18impl ClientConfig {
19 pub fn builder() -> ClientConfigBuilder {
20 ClientConfigBuilder::default()
21 }
22
23 pub fn base_url(&self) -> String {
24 let scheme = if self.tls { "https" } else { "http" };
25 format!("{}://{}:{}{}", scheme, self.host, self.port, self.base_path)
26 }
27}
28
29#[derive(Debug, Default)]
30pub struct ClientConfigBuilder {
31 host: Option<String>,
32 port: Option<u16>,
33 base_path: Option<String>,
34 tls: bool,
35 accept_invalid_certs: bool,
36 timeout_secs: Option<u64>,
37 proxy: Option<String>,
38 proxy_username: Option<String>,
39 proxy_password: Option<String>,
40}
41
42impl ClientConfigBuilder {
43 pub fn host(mut self, host: impl Into<String>) -> Self {
44 self.host = Some(host.into());
45 self
46 }
47
48 pub fn port(mut self, port: u16) -> Self {
49 self.port = Some(port);
50 self
51 }
52
53 pub fn base_path(mut self, path: impl Into<String>) -> Self {
54 self.base_path = Some(normalize_base_path(path.into()));
55 self
56 }
57
58 pub fn tls(mut self, tls: bool) -> Self {
59 self.tls = tls;
60 self
61 }
62
63 pub fn accept_invalid_certs(mut self, accept: bool) -> Self {
64 self.accept_invalid_certs = accept;
65 self
66 }
67
68 pub fn timeout_secs(mut self, secs: u64) -> Self {
69 self.timeout_secs = Some(secs);
70 self
71 }
72
73 pub fn proxy(mut self, url: impl Into<String>) -> Self {
79 self.proxy = Some(url.into());
80 self
81 }
82
83 pub fn proxy_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
86 self.proxy_username = Some(username.into());
87 self.proxy_password = Some(password.into());
88 self
89 }
90
91 pub fn no_proxy(mut self) -> Self {
93 self.proxy = None;
94 self.proxy_username = None;
95 self.proxy_password = None;
96 self
97 }
98
99 pub fn build(self) -> crate::Result<ClientConfig> {
100 let host = self
101 .host
102 .ok_or_else(|| crate::Error::Config("host is required".into()))?;
103 let port = self
104 .port
105 .ok_or_else(|| crate::Error::Config("port is required".into()))?;
106 if port == 0 {
107 return Err(crate::Error::Config("port cannot be 0".into()));
108 }
109 if let Some(url) = &self.proxy {
110 reqwest::Proxy::all(url.as_str())
112 .map_err(|e| crate::Error::Config(format!("invalid proxy url: {}", e)))?;
113 }
114 Ok(ClientConfig {
115 host,
116 port,
117 base_path: self.base_path.unwrap_or_else(|| "/".to_string()),
118 tls: self.tls,
119 accept_invalid_certs: self.accept_invalid_certs,
120 timeout_secs: self.timeout_secs.unwrap_or(30),
121 proxy: self.proxy,
122 proxy_username: self.proxy_username,
123 proxy_password: self.proxy_password,
124 })
125 }
126}
127
128fn normalize_base_path(mut path: String) -> String {
129 if !path.starts_with('/') {
130 path.insert(0, '/');
131 }
132 if !path.ends_with('/') {
133 path.push('/');
134 }
135 path
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn build_minimal_config() {
144 let cfg = ClientConfig::builder()
145 .host("192.168.1.1")
146 .port(2053)
147 .build()
148 .unwrap();
149 assert_eq!(cfg.host, "192.168.1.1");
150 assert_eq!(cfg.port, 2053);
151 assert_eq!(cfg.base_path, "/");
152 assert!(!cfg.tls);
153 assert_eq!(cfg.timeout_secs, 30);
154 }
155
156 #[test]
157 fn build_full_config() {
158 let cfg = ClientConfig::builder()
159 .host("example.com")
160 .port(443)
161 .base_path("secret")
162 .tls(true)
163 .accept_invalid_certs(true)
164 .timeout_secs(60)
165 .build()
166 .unwrap();
167 assert_eq!(cfg.base_path, "/secret/");
168 assert!(cfg.tls);
169 assert!(cfg.accept_invalid_certs);
170 assert_eq!(cfg.timeout_secs, 60);
171 }
172
173 #[test]
174 fn base_url_http() {
175 let cfg = ClientConfig::builder()
176 .host("192.168.1.1")
177 .port(2053)
178 .build()
179 .unwrap();
180 assert_eq!(cfg.base_url(), "http://192.168.1.1:2053/");
181 }
182
183 #[test]
184 fn base_url_https_with_path() {
185 let cfg = ClientConfig::builder()
186 .host("example.com")
187 .port(443)
188 .base_path("/secret/")
189 .tls(true)
190 .build()
191 .unwrap();
192 assert_eq!(cfg.base_url(), "https://example.com:443/secret/");
193 }
194
195 #[test]
196 fn missing_host_errors() {
197 let err = ClientConfig::builder().port(2053).build().unwrap_err();
198 assert!(err.to_string().contains("host is required"));
199 }
200
201 #[test]
202 fn proxy_http_url_accepted() {
203 let cfg = ClientConfig::builder()
204 .host("localhost")
205 .port(2053)
206 .proxy("http://127.0.0.1:8080")
207 .build()
208 .unwrap();
209 assert_eq!(cfg.proxy.as_deref(), Some("http://127.0.0.1:8080"));
210 }
211
212 #[test]
213 fn proxy_socks5_url_accepted() {
214 let cfg = ClientConfig::builder()
215 .host("localhost")
216 .port(2053)
217 .proxy("socks5://127.0.0.1:1080")
218 .proxy_auth("user", "pass")
219 .build()
220 .unwrap();
221 assert_eq!(cfg.proxy_username.as_deref(), Some("user"));
222 assert_eq!(cfg.proxy_password.as_deref(), Some("pass"));
223 }
224
225 #[test]
226 fn proxy_invalid_url_errors() {
227 let err = ClientConfig::builder()
228 .host("localhost")
229 .port(2053)
230 .proxy("not a url at all")
231 .build()
232 .unwrap_err();
233 assert!(err.to_string().contains("invalid proxy url"), "{}", err);
234 }
235
236 #[test]
237 fn no_proxy_clears_settings() {
238 let cfg = ClientConfig::builder()
239 .host("localhost")
240 .port(2053)
241 .proxy("http://1.2.3.4:8080")
242 .proxy_auth("u", "p")
243 .no_proxy()
244 .build()
245 .unwrap();
246 assert!(cfg.proxy.is_none());
247 assert!(cfg.proxy_username.is_none());
248 assert!(cfg.proxy_password.is_none());
249 }
250
251 #[test]
252 fn port_zero_errors() {
253 let err = ClientConfig::builder()
254 .host("localhost")
255 .port(0)
256 .build()
257 .unwrap_err();
258 assert!(err.to_string().contains("port cannot be 0"));
259 }
260}