1use crate::error::{Error, Result};
4use reqwest::Client;
5use reqwest_middleware::ClientBuilder;
6use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
7use std::sync::{Arc, OnceLock};
8use std::time::Duration;
9use tokio::sync::Semaphore;
10
11static GLOBAL_HTTP_CLIENT: OnceLock<Arc<reqwest_middleware::ClientWithMiddleware>> =
17 OnceLock::new();
18
19pub fn init_global_http_client(config: &crate::config::PerformanceConfig) -> Result<()> {
36 if GLOBAL_HTTP_CLIENT.get().is_some() {
38 return Ok(());
39 }
40
41 let client_result = create_http_client_from_config(config).build();
43
44 match client_result {
45 Ok(client) => {
46 let client_arc = Arc::new(client);
47 let _ = GLOBAL_HTTP_CLIENT.set(client_arc);
49 Ok(())
50 }
51 Err(e) => {
52 Err(Error::initialization(
55 "global_http_client",
56 format!("Failed to create global HTTP client: {e}"),
57 ))
58 }
59 }
60}
61
62#[must_use = "returns a Result that should be checked"]
71pub fn get_global_http_client() -> Result<Arc<reqwest_middleware::ClientWithMiddleware>> {
72 GLOBAL_HTTP_CLIENT.get().cloned().ok_or_else(|| {
73 Error::initialization(
74 "global_http_client",
75 "Global HTTP client not initialized. Call init_global_http_client() first.",
76 )
77 })
78}
79
80pub fn get_or_init_global_http_client() -> Result<Arc<reqwest_middleware::ClientWithMiddleware>> {
90 if let Some(client) = GLOBAL_HTTP_CLIENT.get() {
92 return Ok(client.clone());
93 }
94
95 let default_config = crate::config::PerformanceConfig::default();
97 init_global_http_client(&default_config)?;
98
99 GLOBAL_HTTP_CLIENT.get().cloned().ok_or_else(|| {
101 Error::initialization(
102 "global_http_client",
103 "HTTP client initialization failed unexpectedly".to_string(),
104 )
105 })
106}
107
108pub struct HttpClientBuilder {
113 timeout: Duration,
114 connect_timeout: Duration,
115 read_timeout: Duration,
116 pool_max_idle_per_host: usize,
117 pool_idle_timeout: Duration,
118 user_agent: String,
119 enable_gzip: bool,
120 enable_brotli: bool,
121 max_retries: u32,
122 retry_initial_delay: Duration,
123 retry_max_delay: Duration,
124}
125
126impl Default for HttpClientBuilder {
127 fn default() -> Self {
128 Self {
129 timeout: Duration::from_secs(30),
130 connect_timeout: Duration::from_secs(10),
131 read_timeout: Duration::from_secs(30),
132 pool_max_idle_per_host: 10,
133 pool_idle_timeout: Duration::from_secs(90),
134 user_agent: crate::user_agent(),
135 enable_gzip: true,
136 enable_brotli: true,
137 max_retries: 3,
138 retry_initial_delay: Duration::from_millis(100),
139 retry_max_delay: Duration::from_secs(10),
140 }
141 }
142}
143
144impl HttpClientBuilder {
145 #[must_use]
147 pub fn new() -> Self {
148 Self::default()
149 }
150
151 #[must_use]
153 pub fn timeout(mut self, timeout: Duration) -> Self {
154 self.timeout = timeout;
155 self
156 }
157
158 #[must_use]
160 pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
161 self.connect_timeout = connect_timeout;
162 self
163 }
164
165 #[must_use]
167 pub fn read_timeout(mut self, read_timeout: Duration) -> Self {
168 self.read_timeout = read_timeout;
169 self
170 }
171
172 #[must_use]
174 pub fn pool_max_idle_per_host(mut self, max_idle: usize) -> Self {
175 self.pool_max_idle_per_host = max_idle;
176 self
177 }
178
179 #[must_use]
181 pub fn pool_idle_timeout(mut self, idle_timeout: Duration) -> Self {
182 self.pool_idle_timeout = idle_timeout;
183 self
184 }
185
186 #[must_use]
188 pub fn user_agent(mut self, user_agent: String) -> Self {
189 self.user_agent = user_agent;
190 self
191 }
192
193 #[must_use]
195 pub fn enable_gzip(mut self, enable: bool) -> Self {
196 self.enable_gzip = enable;
197 self
198 }
199
200 #[must_use]
202 pub fn enable_brotli(mut self, enable: bool) -> Self {
203 self.enable_brotli = enable;
204 self
205 }
206
207 #[must_use]
209 pub fn max_retries(mut self, max_retries: u32) -> Self {
210 self.max_retries = max_retries;
211 self
212 }
213
214 #[must_use]
216 pub fn retry_initial_delay(mut self, delay: Duration) -> Self {
217 self.retry_initial_delay = delay;
218 self
219 }
220
221 #[must_use]
223 pub fn retry_max_delay(mut self, delay: Duration) -> Self {
224 self.retry_max_delay = delay;
225 self
226 }
227
228 pub fn build(self) -> Result<reqwest_middleware::ClientWithMiddleware> {
238 let mut builder = Client::builder()
239 .timeout(self.timeout)
240 .connect_timeout(self.connect_timeout)
241 .read_timeout(self.read_timeout)
242 .pool_max_idle_per_host(self.pool_max_idle_per_host)
243 .pool_idle_timeout(self.pool_idle_timeout)
244 .user_agent(&self.user_agent);
245
246 if !self.enable_gzip {
249 builder = builder.no_gzip();
250 }
251
252 if !self.enable_brotli {
253 builder = builder.no_brotli();
254 }
255
256 let client = builder
257 .build()
258 .map_err(|e| Error::http_request("BUILD", "client", 0, e.to_string()))?;
259
260 let retry_policy = ExponentialBackoff::builder()
262 .retry_bounds(self.retry_initial_delay, self.retry_max_delay)
263 .build_with_max_retries(self.max_retries);
264
265 Ok(ClientBuilder::new(client)
267 .with(RetryTransientMiddleware::new_with_policy(retry_policy))
268 .build())
269 }
270
271 pub fn build_plain(self) -> Result<Client> {
276 let mut builder = Client::builder()
277 .timeout(self.timeout)
278 .connect_timeout(self.connect_timeout)
279 .read_timeout(self.read_timeout)
280 .pool_max_idle_per_host(self.pool_max_idle_per_host)
281 .pool_idle_timeout(self.pool_idle_timeout)
282 .user_agent(&self.user_agent);
283
284 if !self.enable_gzip {
285 builder = builder.no_gzip();
286 }
287
288 if !self.enable_brotli {
289 builder = builder.no_brotli();
290 }
291
292 builder
293 .build()
294 .map_err(|e| Error::http_request("BUILD", "client", 0, e.to_string()))
295 }
296}
297
298#[must_use]
304pub fn create_http_client_from_config(
305 config: &crate::config::PerformanceConfig,
306) -> HttpClientBuilder {
307 HttpClientBuilder::new()
308 .timeout(Duration::from_secs(config.http_client_timeout_secs))
309 .connect_timeout(Duration::from_secs(config.http_client_connect_timeout_secs))
310 .read_timeout(Duration::from_secs(config.http_client_read_timeout_secs))
311 .pool_max_idle_per_host(config.http_client_pool_size)
312 .pool_idle_timeout(Duration::from_secs(
313 config.http_client_pool_idle_timeout_secs,
314 ))
315 .max_retries(config.http_client_max_retries)
316 .retry_initial_delay(Duration::from_millis(
317 config.http_client_retry_initial_delay_ms,
318 ))
319 .retry_max_delay(Duration::from_millis(config.http_client_retry_max_delay_ms))
320}
321
322pub struct RateLimiter {
324 semaphore: Arc<Semaphore>,
325 max_permits: usize,
326}
327
328impl RateLimiter {
329 #[must_use]
331 pub fn new(max_permits: usize) -> Self {
332 Self {
333 semaphore: Arc::new(Semaphore::new(max_permits)),
334 max_permits,
335 }
336 }
337
338 pub async fn acquire(&self) -> Result<tokio::sync::SemaphorePermit<'_>> {
340 self.semaphore
341 .acquire()
342 .await
343 .map_err(|e| Error::Other(format!("Failed to acquire rate limit permit: {e}")))
344 }
345
346 #[must_use]
348 pub fn try_acquire(&self) -> Option<tokio::sync::SemaphorePermit<'_>> {
349 self.semaphore.try_acquire().ok()
350 }
351
352 #[must_use]
354 pub fn available_permits(&self) -> usize {
355 self.semaphore.available_permits()
356 }
357
358 #[must_use]
360 pub fn max_permits(&self) -> usize {
361 self.max_permits
362 }
363}
364
365pub mod compression {
367 use crate::error::{Error, Result};
368 use flate2::write::GzEncoder;
369 use flate2::Compression;
370 use std::io::Write;
371
372 pub fn gzip_compress(data: &[u8]) -> Result<Vec<u8>> {
374 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
375 encoder
376 .write_all(data)
377 .map_err(|e| Error::Other(format!("Gzip compression failed: {e}")))?;
378 encoder
379 .finish()
380 .map_err(|e| Error::Other(format!("Gzip compression finalize failed: {e}")))
381 }
382
383 pub fn gzip_decompress(data: &[u8]) -> Result<Vec<u8>> {
385 let mut decoder = flate2::read::GzDecoder::new(data);
386 let mut decompressed = Vec::new();
387 std::io::Read::read_to_end(&mut decoder, &mut decompressed)
388 .map_err(|e| Error::Other(format!("Gzip decompression failed: {e}")))?;
389 Ok(decompressed)
390 }
391}
392
393pub mod string {
395 #[must_use]
414 pub fn truncate_with_ellipsis(s: &str, max_len: usize) -> String {
415 if max_len <= 3 {
417 return "...".to_string();
418 }
419
420 let chars: Vec<char> = s.chars().collect();
422
423 if chars.len() <= max_len {
425 return s.to_string();
426 }
427
428 let truncated: String = chars.iter().take(max_len - 3).collect();
430 format!("{truncated}...")
431 }
432
433 pub fn parse_number<T: std::str::FromStr>(s: &str, default: T) -> T {
435 s.parse().unwrap_or(default)
436 }
437
438 #[must_use]
440 pub fn is_blank(s: &str) -> bool {
441 s.trim().is_empty()
442 }
443}
444
445pub mod time {
447 use chrono::{DateTime, Utc};
448
449 #[must_use]
451 pub fn current_timestamp_ms() -> i64 {
452 Utc::now().timestamp_millis()
453 }
454
455 #[must_use]
457 pub fn format_datetime(dt: &DateTime<Utc>) -> String {
458 dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string()
459 }
460
461 #[must_use]
463 pub fn elapsed_ms(start: std::time::Instant) -> u128 {
464 start.elapsed().as_millis()
465 }
466}
467
468pub mod validation {
470 use crate::error::Error;
471
472 const MAX_CRATE_NAME_LENGTH: usize = 100;
474 const MAX_VERSION_LENGTH: usize = 50;
476 const MAX_SEARCH_QUERY_LENGTH: usize = 200;
478
479 pub fn validate_crate_name(name: &str) -> Result<(), Error> {
481 if name.is_empty() {
482 return Err(Error::Other("Crate name cannot be empty".to_string()));
483 }
484
485 if name.len() > MAX_CRATE_NAME_LENGTH {
486 return Err(Error::Other("Crate name is too long".to_string()));
487 }
488
489 if !name
491 .chars()
492 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
493 {
494 return Err(Error::Other(
495 "Crate name contains invalid characters".to_string(),
496 ));
497 }
498
499 Ok(())
500 }
501
502 pub fn validate_version(version: &str) -> Result<(), Error> {
504 if version.is_empty() {
505 return Err(Error::Other("Version cannot be empty".to_string()));
506 }
507
508 if version.len() > MAX_VERSION_LENGTH {
509 return Err(Error::Other("Version is too long".to_string()));
510 }
511
512 if !version.chars().any(|c| c.is_ascii_digit()) {
514 return Err(Error::Other("Version must contain digits".to_string()));
515 }
516
517 Ok(())
518 }
519
520 pub fn validate_search_query(query: &str) -> Result<(), Error> {
522 if query.is_empty() {
523 return Err(Error::Other("Search query cannot be empty".to_string()));
524 }
525
526 if query.len() > MAX_SEARCH_QUERY_LENGTH {
527 return Err(Error::Other("Search query is too long".to_string()));
528 }
529
530 Ok(())
531 }
532}
533
534pub mod metrics {
536 use std::sync::atomic::{AtomicU64, Ordering};
537 use std::sync::Arc;
538 use std::time::Instant;
539
540 #[derive(Clone)]
542 pub struct PerformanceCounter {
543 total_requests: Arc<AtomicU64>,
544 successful_requests: Arc<AtomicU64>,
545 failed_requests: Arc<AtomicU64>,
546 total_response_time_ms: Arc<AtomicU64>,
547 }
548
549 impl PerformanceCounter {
550 #[must_use]
552 pub fn new() -> Self {
553 Self {
554 total_requests: Arc::new(AtomicU64::new(0)),
555 successful_requests: Arc::new(AtomicU64::new(0)),
556 failed_requests: Arc::new(AtomicU64::new(0)),
557 total_response_time_ms: Arc::new(AtomicU64::new(0)),
558 }
559 }
560
561 #[must_use]
563 pub fn record_request_start(&self) -> Instant {
564 self.total_requests.fetch_add(1, Ordering::Relaxed);
565 Instant::now()
566 }
567
568 #[allow(clippy::cast_possible_truncation)]
570 pub fn record_request_complete(&self, start: Instant, success: bool) {
571 let duration_ms = start.elapsed().as_millis() as u64;
572 self.total_response_time_ms
573 .fetch_add(duration_ms, Ordering::Relaxed);
574
575 if success {
576 self.successful_requests.fetch_add(1, Ordering::Relaxed);
577 } else {
578 self.failed_requests.fetch_add(1, Ordering::Relaxed);
579 }
580 }
581
582 #[must_use]
584 pub fn get_stats(&self) -> PerformanceStats {
585 let total = self.total_requests.load(Ordering::Relaxed);
586 let success = self.successful_requests.load(Ordering::Relaxed);
587 let failed = self.failed_requests.load(Ordering::Relaxed);
588 let total_time = self.total_response_time_ms.load(Ordering::Relaxed);
589
590 #[allow(clippy::cast_precision_loss)]
591 let avg_response_time = if total > 0 {
592 total_time as f64 / total as f64
593 } else {
594 0.0
595 };
596
597 #[allow(clippy::cast_precision_loss)]
598 let success_rate = if total > 0 {
599 success as f64 / total as f64 * 100.0
600 } else {
601 0.0
602 };
603
604 PerformanceStats {
605 total_requests: total,
606 successful_requests: success,
607 failed_requests: failed,
608 average_response_time_ms: avg_response_time,
609 success_rate_percent: success_rate,
610 }
611 }
612
613 pub fn reset(&self) {
615 self.total_requests.store(0, Ordering::Relaxed);
616 self.successful_requests.store(0, Ordering::Relaxed);
617 self.failed_requests.store(0, Ordering::Relaxed);
618 self.total_response_time_ms.store(0, Ordering::Relaxed);
619 }
620 }
621
622 impl Default for PerformanceCounter {
623 fn default() -> Self {
624 Self::new()
625 }
626 }
627
628 #[derive(Debug, Clone, serde::Serialize)]
630 pub struct PerformanceStats {
631 pub total_requests: u64,
633 pub successful_requests: u64,
635 pub failed_requests: u64,
637 pub average_response_time_ms: f64,
639 pub success_rate_percent: f64,
641 }
642}