1use std::{
2 sync::{Arc, LazyLock, RwLock},
3 time::Duration,
4};
5
6use ureq::{
7 http::{self, HeaderMap, Uri},
8 typestate::{WithBody, WithoutBody},
9 Agent, Proxy, RequestBuilder,
10};
11
12#[derive(Clone, Debug)]
13pub struct ClientConfig {
14 pub user_agent: Option<String>,
15 pub headers: Option<HeaderMap>,
16 pub proxy: Option<Proxy>,
17 pub timeout: Option<Duration>,
18}
19
20impl Default for ClientConfig {
21 fn default() -> Self {
37 Self {
38 user_agent: Some("pkgforge/soar".into()),
39 proxy: None,
40 headers: None,
41 timeout: None,
42 }
43 }
44}
45
46impl ClientConfig {
47 pub fn build(&self) -> Agent {
63 let mut config = ureq::Agent::config_builder()
64 .proxy(self.proxy.clone())
65 .timeout_global(self.timeout);
66
67 if let Some(user_agent) = &self.user_agent {
68 config = config.user_agent(user_agent);
69 }
70
71 config.build().into()
72 }
73}
74
75struct SharedClient {
76 agent: Agent,
77 config: ClientConfig,
78}
79
80static SHARED_CLIENT_STATE: LazyLock<Arc<RwLock<SharedClient>>> = LazyLock::new(|| {
81 let config = ClientConfig::default();
82 let agent = config.build();
83
84 Arc::new(RwLock::new(SharedClient {
85 agent,
86 config,
87 }))
88});
89
90#[derive(Clone, Default)]
91pub struct SharedAgent;
92
93impl SharedAgent {
94 pub fn new() -> Self {
104 Self
105 }
106
107 pub fn head<T>(&self, uri: T) -> RequestBuilder<WithoutBody>
108 where
109 Uri: TryFrom<T>,
110 <Uri as TryFrom<T>>::Error: Into<http::Error>,
111 {
112 let state = SHARED_CLIENT_STATE.read().unwrap();
113 let req = state.agent.head(uri);
114 apply_headers(req, &state.config.headers)
115 }
116
117 pub fn get<T>(&self, uri: T) -> RequestBuilder<WithoutBody>
131 where
132 Uri: TryFrom<T>,
133 <Uri as TryFrom<T>>::Error: Into<http::Error>,
134 {
135 let state = SHARED_CLIENT_STATE.read().unwrap();
136 let req = state.agent.get(uri);
137 apply_headers(req, &state.config.headers)
138 }
139
140 pub fn post<T>(&self, uri: T) -> RequestBuilder<WithBody>
152 where
153 Uri: TryFrom<T>,
154 <Uri as TryFrom<T>>::Error: Into<http::Error>,
155 {
156 let state = SHARED_CLIENT_STATE.read().unwrap();
157 let req = state.agent.post(uri);
158 apply_headers(req, &state.config.headers)
159 }
160
161 pub fn put<T>(&self, uri: T) -> RequestBuilder<WithBody>
171 where
172 Uri: TryFrom<T>,
173 <Uri as TryFrom<T>>::Error: Into<http::Error>,
174 {
175 let state = SHARED_CLIENT_STATE.read().unwrap();
176 let req = state.agent.put(uri);
177 apply_headers(req, &state.config.headers)
178 }
179
180 pub fn delete<T>(&self, uri: T) -> RequestBuilder<WithoutBody>
195 where
196 Uri: TryFrom<T>,
197 <Uri as TryFrom<T>>::Error: Into<http::Error>,
198 {
199 let state = SHARED_CLIENT_STATE.read().unwrap();
200 let req = state.agent.delete(uri);
201 apply_headers(req, &state.config.headers)
202 }
203}
204
205fn apply_headers<B>(mut req: RequestBuilder<B>, headers: &Option<HeaderMap>) -> RequestBuilder<B> {
211 if let Some(headers) = headers {
212 for (key, value) in headers.iter() {
213 req = req.header(key, value);
214 }
215 }
216 req
217}
218
219pub static SHARED_AGENT: LazyLock<SharedAgent> = LazyLock::new(SharedAgent::new);
220
221pub fn configure_http_client<F>(updater: F)
237where
238 F: FnOnce(&mut ClientConfig),
239{
240 let mut state = SHARED_CLIENT_STATE.write().unwrap();
241 let mut new_config = state.config.clone();
242 updater(&mut new_config);
243 let new_agent = new_config.build();
244 state.agent = new_agent;
245 state.config = new_config;
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_client_config_default() {
254 let config = ClientConfig::default();
255 assert_eq!(config.user_agent, Some("pkgforge/soar".to_string()));
256 assert!(config.proxy.is_none());
257 assert!(config.headers.is_none());
258 assert!(config.timeout.is_none());
259 }
260
261 #[test]
262 fn test_client_config_build() {
263 let config = ClientConfig::default();
264 let agent = config.build();
265 let _ = agent;
267 }
268
269 #[test]
270 fn test_client_config_with_timeout() {
271 let config = ClientConfig {
272 user_agent: Some("test-agent".to_string()),
273 proxy: None,
274 headers: None,
275 timeout: Some(Duration::from_secs(30)),
276 };
277 let agent = config.build();
278 let _ = agent;
279 }
280
281 #[test]
282 fn test_shared_agent_new() {
283 let agent = SharedAgent::new();
284 let _ = agent;
285 }
286
287 #[test]
288 fn test_shared_agent_get() {
289 let agent = SharedAgent::new();
290 let req = agent.get("https://example.com");
291 let _ = req;
293 }
294
295 #[test]
296 fn test_shared_agent_post() {
297 let agent = SharedAgent::new();
298 let req = agent.post("https://example.com");
299 let _ = req;
300 }
301
302 #[test]
303 fn test_shared_agent_put() {
304 let agent = SharedAgent::new();
305 let req = agent.put("https://example.com");
306 let _ = req;
307 }
308
309 #[test]
310 fn test_shared_agent_delete() {
311 let agent = SharedAgent::new();
312 let req = agent.delete("https://example.com");
313 let _ = req;
314 }
315
316 #[test]
317 fn test_shared_agent_head() {
318 let agent = SharedAgent::new();
319 let req = agent.head("https://example.com");
320 let _ = req;
321 }
322
323 #[test]
324 fn test_configure_http_client() {
325 configure_http_client(|cfg| {
326 cfg.user_agent = Some("custom-agent/1.0".to_string());
327 });
328
329 let agent = SharedAgent::new();
331 let _ = agent.get("https://example.com");
332 }
333
334 #[test]
335 fn test_configure_http_client_timeout() {
336 configure_http_client(|cfg| {
337 cfg.timeout = Some(Duration::from_secs(10));
338 });
339
340 let agent = SharedAgent::new();
341 let _ = agent.get("https://example.com");
342 }
343
344 #[test]
345 fn test_shared_agent_clone() {
346 let agent1 = SharedAgent::new();
347 let agent2 = agent1.clone();
348
349 let _ = agent1.get("https://example.com");
351 let _ = agent2.get("https://example.com");
352 }
353
354 #[test]
355 fn test_shared_agent_default() {
356 let agent = SharedAgent;
357 let _ = agent.get("https://example.com");
358 }
359
360 #[test]
361 fn test_apply_headers_none() {
362 let agent: ureq::Agent = ureq::Agent::config_builder().build().into();
363 let req = agent.get("https://example.com");
364 let req = apply_headers(req, &None);
365 let _ = req;
366 }
367
368 #[test]
369 fn test_apply_headers_some() {
370 let agent: ureq::Agent = ureq::Agent::config_builder().build().into();
371 let req = agent.get("https://example.com");
372
373 let mut headers = ureq::http::HeaderMap::new();
374 headers.insert(
375 ureq::http::header::USER_AGENT,
376 ureq::http::HeaderValue::from_static("test-agent"),
377 );
378
379 let req = apply_headers(req, &Some(headers));
380 let _ = req;
381 }
382
383 #[test]
384 fn test_client_config_clone() {
385 let config1 = ClientConfig::default();
386 let config2 = config1.clone();
387
388 assert_eq!(config1.user_agent, config2.user_agent);
389 }
390
391 #[test]
392 fn test_client_config_debug() {
393 let config = ClientConfig::default();
394 let debug = format!("{:?}", config);
395 assert!(debug.contains("ClientConfig"));
396 }
397}