Skip to main content

crates_docs/utils/
mod.rs

1//! Utility functions module
2
3use 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
11/// Global HTTP client singleton with connection pool reuse
12///
13/// This static instance ensures connection pooling is effective across
14/// all HTTP requests in the application. The client is lazily initialized
15/// on first access.
16static GLOBAL_HTTP_CLIENT: OnceLock<Arc<reqwest_middleware::ClientWithMiddleware>> =
17    OnceLock::new();
18
19/// Initialize the global HTTP client singleton
20///
21/// # Arguments
22///
23/// * `config` - Performance configuration for connection pool settings
24///
25/// # Errors
26///
27/// Returns an error if HTTP client creation fails
28///
29/// # Note
30///
31/// This function is thread-safe. The first successful initialization wins and
32/// is reused by all callers. A failed attempt is *not* cached: because client
33/// creation can fail for transient reasons (e.g. TLS backend or resource
34/// pressure), a subsequent call is free to retry and may succeed.
35pub fn init_global_http_client(config: &crate::config::PerformanceConfig) -> Result<()> {
36    // Fast path: already initialized
37    if GLOBAL_HTTP_CLIENT.get().is_some() {
38        return Ok(());
39    }
40
41    // Slow path: try to initialize
42    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            // set() returns Err if already initialized, which is fine
48            let _ = GLOBAL_HTTP_CLIENT.set(client_arc);
49            Ok(())
50        }
51        Err(e) => {
52            // Do not cache the failure: client creation can fail transiently,
53            // so a later call is allowed to retry.
54            Err(Error::initialization(
55                "global_http_client",
56                format!("Failed to create global HTTP client: {e}"),
57            ))
58        }
59    }
60}
61
62/// Get the global HTTP client singleton
63///
64/// # Errors
65///
66/// Returns an error if the global HTTP client has not been initialized.
67/// Call `init_global_http_client()` before using this function.
68///
69/// If you need automatic initialization, use `get_or_init_global_http_client()` instead.
70#[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
80/// Get or initialize the global HTTP client with default config
81///
82/// This is a convenience function for use cases where the client
83/// might not be explicitly initialized. It uses default performance config.
84///
85/// # Errors
86///
87/// Returns an error if HTTP client creation fails (e.g., TLS initialization error).
88/// This function is thread-safe and ensures only one thread performs initialization.
89pub fn get_or_init_global_http_client() -> Result<Arc<reqwest_middleware::ClientWithMiddleware>> {
90    // Fast path: already initialized
91    if let Some(client) = GLOBAL_HTTP_CLIENT.get() {
92        return Ok(client.clone());
93    }
94
95    // Use init_global_http_client with default config for thread-safe initialization
96    let default_config = crate::config::PerformanceConfig::default();
97    init_global_http_client(&default_config)?;
98
99    // Now it should be initialized
100    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
108/// HTTP client builder with retry support
109///
110/// This builder creates a `reqwest_middleware::ClientWithMiddleware` that includes
111/// automatic retry functionality for transient failures.
112pub 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    /// Create a new HTTP client builder
146    #[must_use]
147    pub fn new() -> Self {
148        Self::default()
149    }
150
151    /// Set request timeout
152    #[must_use]
153    pub fn timeout(mut self, timeout: Duration) -> Self {
154        self.timeout = timeout;
155        self
156    }
157
158    /// Set connection timeout
159    #[must_use]
160    pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
161        self.connect_timeout = connect_timeout;
162        self
163    }
164
165    /// Set read timeout
166    #[must_use]
167    pub fn read_timeout(mut self, read_timeout: Duration) -> Self {
168        self.read_timeout = read_timeout;
169        self
170    }
171
172    /// Set connection pool size
173    #[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    /// Set pool idle timeout
180    #[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    /// Set User-Agent
187    #[must_use]
188    pub fn user_agent(mut self, user_agent: String) -> Self {
189        self.user_agent = user_agent;
190        self
191    }
192
193    /// Enable/disable Gzip compression
194    #[must_use]
195    pub fn enable_gzip(mut self, enable: bool) -> Self {
196        self.enable_gzip = enable;
197        self
198    }
199
200    /// Enable/disable Brotli compression
201    #[must_use]
202    pub fn enable_brotli(mut self, enable: bool) -> Self {
203        self.enable_brotli = enable;
204        self
205    }
206
207    /// Set max retry attempts
208    #[must_use]
209    pub fn max_retries(mut self, max_retries: u32) -> Self {
210        self.max_retries = max_retries;
211        self
212    }
213
214    /// Set retry initial delay
215    #[must_use]
216    pub fn retry_initial_delay(mut self, delay: Duration) -> Self {
217        self.retry_initial_delay = delay;
218        self
219    }
220
221    /// Set retry max delay
222    #[must_use]
223    pub fn retry_max_delay(mut self, delay: Duration) -> Self {
224        self.retry_max_delay = delay;
225        self
226    }
227
228    /// Build HTTP client with middleware chain
229    ///
230    /// This method builds a `reqwest_middleware::ClientWithMiddleware` that includes
231    /// automatic retry functionality using exponential backoff for transient failures.
232    ///
233    /// # Returns
234    ///
235    /// Returns a `ClientWithMiddleware` that can be used like a regular `reqwest::Client`
236    /// but with automatic retry on transient errors.
237    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        // reqwest 0.13 enables gzip and brotli by default
247        // To disable, use .no_gzip() and .no_brotli()
248        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        // Create retry policy with exponential backoff
261        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        // Build client with retry middleware
266        Ok(ClientBuilder::new(client)
267            .with(RetryTransientMiddleware::new_with_policy(retry_policy))
268            .build())
269    }
270
271    /// Build HTTP client without retry support
272    ///
273    /// This method returns a plain `reqwest::Client` without any middleware.
274    /// Use [`build`](Self::build) for retry support.
275    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/// Create HTTP client builder from performance config
299///
300/// This function creates an `HttpClientBuilder` pre-configured with settings
301/// from the provided `PerformanceConfig`. The resulting client will include
302/// automatic retry functionality.
303#[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
322/// Rate limiter
323pub struct RateLimiter {
324    semaphore: Arc<Semaphore>,
325    max_permits: usize,
326}
327
328impl RateLimiter {
329    /// Create a new rate limiter
330    #[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    /// Acquire permit (blocks until available)
339    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    /// Try to acquire permit (non-blocking)
347    #[must_use]
348    pub fn try_acquire(&self) -> Option<tokio::sync::SemaphorePermit<'_>> {
349        self.semaphore.try_acquire().ok()
350    }
351
352    /// Get current number of available permits
353    #[must_use]
354    pub fn available_permits(&self) -> usize {
355        self.semaphore.available_permits()
356    }
357
358    /// Get maximum number of permits
359    #[must_use]
360    pub fn max_permits(&self) -> usize {
361        self.max_permits
362    }
363}
364
365/// Response compression utilities
366pub mod compression {
367    use crate::error::{Error, Result};
368    use flate2::write::GzEncoder;
369    use flate2::Compression;
370    use std::io::Write;
371
372    /// Compress data (Gzip)
373    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    /// Decompress data (Gzip)
384    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
393/// String utilities
394pub mod string {
395    /// Truncate string and add ellipsis (UTF-8 safe)
396    ///
397    /// # Arguments
398    /// * `s` - The string to truncate
399    /// * `max_len` - Maximum number of characters (not bytes) to keep, including ellipsis
400    ///
401    /// # Examples
402    /// ```
403    /// use crates_docs::utils::string::truncate_with_ellipsis;
404    /// // Basic ASCII truncation
405    /// assert_eq!(truncate_with_ellipsis("hello world", 8), "hello...");
406    /// assert_eq!(truncate_with_ellipsis("short", 10), "short");
407    /// // UTF-8 safe: works with multi-byte characters
408    /// assert_eq!(truncate_with_ellipsis("你好世界", 3), "...");
409    /// assert_eq!(truncate_with_ellipsis("你好世界", 4), "你好世界"); // 4 chars <= max_len, no truncation
410    /// assert_eq!(truncate_with_ellipsis("你好世界", 5), "你好世界"); // 4 chars <= max_len, no truncation
411    /// assert_eq!(truncate_with_ellipsis("你好世界你好", 4), "你...");   // 4 chars > max_len-3, truncate
412    /// ```
413    #[must_use]
414    pub fn truncate_with_ellipsis(s: &str, max_len: usize) -> String {
415        // If max_len is 3 or less, just return ellipsis
416        if max_len <= 3 {
417            return "...".to_string();
418        }
419
420        // Collect characters to properly handle UTF-8
421        let chars: Vec<char> = s.chars().collect();
422
423        // If string is short enough, return it as-is
424        if chars.len() <= max_len {
425            return s.to_string();
426        }
427
428        // Truncate to max_len - 3 characters and add ellipsis
429        let truncated: String = chars.iter().take(max_len - 3).collect();
430        format!("{truncated}...")
431    }
432
433    /// Safely parse number
434    pub fn parse_number<T: std::str::FromStr>(s: &str, default: T) -> T {
435        s.parse().unwrap_or(default)
436    }
437
438    /// Check if string is empty or blank
439    #[must_use]
440    pub fn is_blank(s: &str) -> bool {
441        s.trim().is_empty()
442    }
443}
444
445/// Time utilities
446pub mod time {
447    use chrono::{DateTime, Utc};
448
449    /// Get current timestamp (milliseconds)
450    #[must_use]
451    pub fn current_timestamp_ms() -> i64 {
452        Utc::now().timestamp_millis()
453    }
454
455    /// Format datetime
456    #[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    /// Calculate elapsed time (milliseconds)
462    #[must_use]
463    pub fn elapsed_ms(start: std::time::Instant) -> u128 {
464        start.elapsed().as_millis()
465    }
466}
467
468/// Validation utilities
469pub mod validation {
470    use crate::error::Error;
471
472    /// Maximum allowed length for crate names (100 characters)
473    const MAX_CRATE_NAME_LENGTH: usize = 100;
474    /// Maximum allowed length for version strings (50 characters)
475    const MAX_VERSION_LENGTH: usize = 50;
476    /// Maximum allowed length for search queries (200 characters)
477    const MAX_SEARCH_QUERY_LENGTH: usize = 200;
478
479    /// Validate crate name
480    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        // Basic validation: only allow letters, digits, underscores, hyphens
490        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    /// Validate version number
503    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        // Simple validation: should contain digits and dots
513        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    /// Validate search query
521    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
534/// Performance monitoring
535pub mod metrics {
536    use std::sync::atomic::{AtomicU64, Ordering};
537    use std::sync::Arc;
538    use std::time::Instant;
539
540    /// Performance counter
541    #[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        /// Create a new performance counter
551        #[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        /// Record request start
562        #[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        /// Record request completion
569        #[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        /// Get statistics
583        #[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        /// Reset counter
614        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    /// Performance statistics
629    #[derive(Debug, Clone, serde::Serialize)]
630    pub struct PerformanceStats {
631        /// Total requests
632        pub total_requests: u64,
633        /// Successful requests
634        pub successful_requests: u64,
635        /// Failed requests
636        pub failed_requests: u64,
637        /// Average response time (milliseconds)
638        pub average_response_time_ms: f64,
639        /// Success rate (percentage)
640        pub success_rate_percent: f64,
641    }
642}