1use std::{
2 sync::{Arc, LazyLock, RwLock},
3 time::Duration,
4};
5
6use ureq::{
7 config::IpFamily,
8 http::{self, HeaderMap, Uri},
9 typestate::{WithBody, WithoutBody},
10 Agent, Proxy, RequestBuilder,
11};
12
13const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
16
17#[derive(Clone, Debug)]
18pub struct ClientConfig {
19 pub user_agent: Option<String>,
20 pub headers: Option<HeaderMap>,
21 pub proxy: Option<Proxy>,
22 pub timeout: Option<Duration>,
23 pub ip_family: IpFamily,
24}
25
26impl Default for ClientConfig {
27 fn default() -> Self {
46 Self {
47 user_agent: Some("pkgforge/soar".into()),
48 proxy: None,
49 headers: None,
50 timeout: None,
51 ip_family: IpFamily::Any,
52 }
53 }
54}
55
56impl ClientConfig {
57 pub fn build(&self) -> Agent {
76 let mut config = ureq::Agent::config_builder()
77 .timeout_global(self.timeout)
78 .timeout_connect(Some(CONNECT_TIMEOUT))
79 .ip_family(self.ip_family);
80
81 if self.proxy.is_some() {
82 config = config.proxy(self.proxy.clone());
83 }
84
85 if let Some(user_agent) = &self.user_agent {
86 config = config.user_agent(user_agent);
87 }
88
89 config.build().into()
90 }
91}
92
93struct SharedClient {
94 agent: Agent,
95 config: ClientConfig,
96}
97
98static SHARED_CLIENT_STATE: LazyLock<Arc<RwLock<SharedClient>>> = LazyLock::new(|| {
99 let config = ClientConfig::default();
100 let agent = config.build();
101
102 Arc::new(RwLock::new(SharedClient {
103 agent,
104 config,
105 }))
106});
107
108#[derive(Clone, Default)]
109pub struct SharedAgent;
110
111impl SharedAgent {
112 pub fn new() -> Self {
122 Self
123 }
124
125 pub fn head<T>(&self, uri: T) -> RequestBuilder<WithoutBody>
126 where
127 Uri: TryFrom<T>,
128 <Uri as TryFrom<T>>::Error: Into<http::Error>,
129 {
130 let state = SHARED_CLIENT_STATE.read().unwrap();
131 let req = state.agent.head(uri);
132 apply_headers(req, &state.config.headers)
133 }
134
135 pub fn get<T>(&self, uri: T) -> RequestBuilder<WithoutBody>
149 where
150 Uri: TryFrom<T>,
151 <Uri as TryFrom<T>>::Error: Into<http::Error>,
152 {
153 let state = SHARED_CLIENT_STATE.read().unwrap();
154 let req = state.agent.get(uri);
155 apply_headers(req, &state.config.headers)
156 }
157
158 pub fn post<T>(&self, uri: T) -> RequestBuilder<WithBody>
170 where
171 Uri: TryFrom<T>,
172 <Uri as TryFrom<T>>::Error: Into<http::Error>,
173 {
174 let state = SHARED_CLIENT_STATE.read().unwrap();
175 let req = state.agent.post(uri);
176 apply_headers(req, &state.config.headers)
177 }
178
179 pub fn put<T>(&self, uri: T) -> RequestBuilder<WithBody>
189 where
190 Uri: TryFrom<T>,
191 <Uri as TryFrom<T>>::Error: Into<http::Error>,
192 {
193 let state = SHARED_CLIENT_STATE.read().unwrap();
194 let req = state.agent.put(uri);
195 apply_headers(req, &state.config.headers)
196 }
197
198 pub fn delete<T>(&self, uri: T) -> RequestBuilder<WithoutBody>
213 where
214 Uri: TryFrom<T>,
215 <Uri as TryFrom<T>>::Error: Into<http::Error>,
216 {
217 let state = SHARED_CLIENT_STATE.read().unwrap();
218 let req = state.agent.delete(uri);
219 apply_headers(req, &state.config.headers)
220 }
221}
222
223fn apply_headers<B>(mut req: RequestBuilder<B>, headers: &Option<HeaderMap>) -> RequestBuilder<B> {
229 if let Some(headers) = headers {
230 for (key, value) in headers.iter() {
231 req = req.header(key, value);
232 }
233 }
234 req
235}
236
237pub static SHARED_AGENT: LazyLock<SharedAgent> = LazyLock::new(SharedAgent::new);
238
239pub fn configure_http_client<F>(updater: F)
255where
256 F: FnOnce(&mut ClientConfig),
257{
258 let mut state = SHARED_CLIENT_STATE.write().unwrap();
259 let mut new_config = state.config.clone();
260 updater(&mut new_config);
261 let new_agent = new_config.build();
262 state.agent = new_agent;
263 state.config = new_config;
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_client_config_default() {
272 let config = ClientConfig::default();
273 assert_eq!(config.user_agent, Some("pkgforge/soar".to_string()));
274 assert!(config.proxy.is_none());
275 assert!(config.headers.is_none());
276 assert!(config.timeout.is_none());
277 assert_eq!(config.ip_family, IpFamily::Any);
278 }
279
280 #[test]
281 fn test_client_config_build() {
282 let config = ClientConfig::default();
283 let agent = config.build();
284 let _ = agent;
286 }
287
288 #[test]
289 fn test_client_config_with_timeout() {
290 let config = ClientConfig {
291 user_agent: Some("test-agent".to_string()),
292 proxy: None,
293 headers: None,
294 timeout: Some(Duration::from_secs(30)),
295 ip_family: IpFamily::Any,
296 };
297 let agent = config.build();
298 let _ = agent;
299 }
300
301 #[test]
302 fn test_client_config_sets_connect_timeout() {
303 let agent = ClientConfig::default().build();
304 assert_eq!(agent.config().timeouts().connect, Some(CONNECT_TIMEOUT));
305 }
306
307 #[test]
308 fn test_client_config_without_proxy_falls_back_to_env() {
309 let agent = ClientConfig::default().build();
310 assert_eq!(
311 agent.config().proxy().is_some(),
312 Proxy::try_from_env().is_some()
313 );
314 }
315
316 #[test]
317 fn test_client_config_explicit_proxy_overrides_env() {
318 let config = ClientConfig {
319 proxy: Some(Proxy::new("http://127.0.0.1:8080").unwrap()),
320 ..Default::default()
321 };
322 let agent = config.build();
323 assert_eq!(agent.config().proxy().unwrap().port(), 8080);
324 }
325
326 #[test]
327 fn test_client_config_ip_family() {
328 for family in [IpFamily::Any, IpFamily::Ipv4Only, IpFamily::Ipv6Only] {
329 let config = ClientConfig {
330 ip_family: family,
331 ..Default::default()
332 };
333 let agent = config.build();
334 assert_eq!(agent.config().ip_family(), family);
335 }
336 }
337
338 #[test]
339 fn test_shared_agent_new() {
340 let agent = SharedAgent::new();
341 let _ = agent;
342 }
343
344 #[test]
345 fn test_shared_agent_get() {
346 let agent = SharedAgent::new();
347 let req = agent.get("https://example.com");
348 let _ = req;
350 }
351
352 #[test]
353 fn test_shared_agent_post() {
354 let agent = SharedAgent::new();
355 let req = agent.post("https://example.com");
356 let _ = req;
357 }
358
359 #[test]
360 fn test_shared_agent_put() {
361 let agent = SharedAgent::new();
362 let req = agent.put("https://example.com");
363 let _ = req;
364 }
365
366 #[test]
367 fn test_shared_agent_delete() {
368 let agent = SharedAgent::new();
369 let req = agent.delete("https://example.com");
370 let _ = req;
371 }
372
373 #[test]
374 fn test_shared_agent_head() {
375 let agent = SharedAgent::new();
376 let req = agent.head("https://example.com");
377 let _ = req;
378 }
379
380 #[test]
381 fn test_configure_http_client() {
382 configure_http_client(|cfg| {
383 cfg.user_agent = Some("custom-agent/1.0".to_string());
384 });
385
386 let agent = SharedAgent::new();
388 let _ = agent.get("https://example.com");
389 }
390
391 #[test]
392 fn test_configure_http_client_timeout() {
393 configure_http_client(|cfg| {
394 cfg.timeout = Some(Duration::from_secs(10));
395 });
396
397 let agent = SharedAgent::new();
398 let _ = agent.get("https://example.com");
399 }
400
401 #[test]
402 fn test_shared_agent_clone() {
403 let agent1 = SharedAgent::new();
404 let agent2 = agent1.clone();
405
406 let _ = agent1.get("https://example.com");
408 let _ = agent2.get("https://example.com");
409 }
410
411 #[test]
412 fn test_shared_agent_default() {
413 let agent = SharedAgent;
414 let _ = agent.get("https://example.com");
415 }
416
417 #[test]
418 fn test_apply_headers_none() {
419 let agent: ureq::Agent = ureq::Agent::config_builder().build().into();
420 let req = agent.get("https://example.com");
421 let req = apply_headers(req, &None);
422 let _ = req;
423 }
424
425 #[test]
426 fn test_apply_headers_some() {
427 let agent: ureq::Agent = ureq::Agent::config_builder().build().into();
428 let req = agent.get("https://example.com");
429
430 let mut headers = ureq::http::HeaderMap::new();
431 headers.insert(
432 ureq::http::header::USER_AGENT,
433 ureq::http::HeaderValue::from_static("test-agent"),
434 );
435
436 let req = apply_headers(req, &Some(headers));
437 let _ = req;
438 }
439
440 #[test]
441 fn test_client_config_clone() {
442 let config1 = ClientConfig::default();
443 let config2 = config1.clone();
444
445 assert_eq!(config1.user_agent, config2.user_agent);
446 }
447
448 #[test]
449 fn test_client_config_debug() {
450 let config = ClientConfig::default();
451 let debug = format!("{:?}", config);
452 assert!(debug.contains("ClientConfig"));
453 }
454}