Skip to main content

crates_docs/utils/
mod.rs

1//! Utility functions module
2
3use crate::error::{Error, Result};
4use reqwest::Client;
5use std::sync::Arc;
6use std::time::Duration;
7use tokio::sync::Semaphore;
8
9/// HTTP client builder with retry support
10pub struct HttpClientBuilder {
11    timeout: Duration,
12    connect_timeout: Duration,
13    read_timeout: Duration,
14    pool_max_idle_per_host: usize,
15    pool_idle_timeout: Duration,
16    user_agent: String,
17    enable_gzip: bool,
18    enable_brotli: bool,
19    max_retries: u32,
20    retry_initial_delay: Duration,
21    retry_max_delay: Duration,
22}
23
24impl Default for HttpClientBuilder {
25    fn default() -> Self {
26        Self {
27            timeout: Duration::from_secs(30),
28            connect_timeout: Duration::from_secs(10),
29            read_timeout: Duration::from_secs(30),
30            pool_max_idle_per_host: 10,
31            pool_idle_timeout: Duration::from_secs(90),
32            user_agent: format!("CratesDocsMCP/{}", crate::VERSION),
33            enable_gzip: true,
34            enable_brotli: true,
35            max_retries: 3,
36            retry_initial_delay: Duration::from_millis(100),
37            retry_max_delay: Duration::from_secs(10),
38        }
39    }
40}
41
42impl HttpClientBuilder {
43    /// Create a new HTTP client builder
44    #[must_use]
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Set request timeout
50    #[must_use]
51    pub fn timeout(mut self, timeout: Duration) -> Self {
52        self.timeout = timeout;
53        self
54    }
55
56    /// Set connection timeout
57    #[must_use]
58    pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
59        self.connect_timeout = connect_timeout;
60        self
61    }
62
63    /// Set read timeout
64    #[must_use]
65    pub fn read_timeout(mut self, read_timeout: Duration) -> Self {
66        self.read_timeout = read_timeout;
67        self
68    }
69
70    /// Set connection pool size
71    #[must_use]
72    pub fn pool_max_idle_per_host(mut self, max_idle: usize) -> Self {
73        self.pool_max_idle_per_host = max_idle;
74        self
75    }
76
77    /// Set pool idle timeout
78    #[must_use]
79    pub fn pool_idle_timeout(mut self, idle_timeout: Duration) -> Self {
80        self.pool_idle_timeout = idle_timeout;
81        self
82    }
83
84    /// Set User-Agent
85    #[must_use]
86    pub fn user_agent(mut self, user_agent: String) -> Self {
87        self.user_agent = user_agent;
88        self
89    }
90
91    /// Enable/disable Gzip compression
92    #[must_use]
93    pub fn enable_gzip(mut self, enable: bool) -> Self {
94        self.enable_gzip = enable;
95        self
96    }
97
98    /// Enable/disable Brotli compression
99    #[must_use]
100    pub fn enable_brotli(mut self, enable: bool) -> Self {
101        self.enable_brotli = enable;
102        self
103    }
104
105    /// Set max retry attempts
106    #[must_use]
107    pub fn max_retries(mut self, max_retries: u32) -> Self {
108        self.max_retries = max_retries;
109        self
110    }
111
112    /// Set retry initial delay
113    #[must_use]
114    pub fn retry_initial_delay(mut self, delay: Duration) -> Self {
115        self.retry_initial_delay = delay;
116        self
117    }
118
119    /// Set retry max delay
120    #[must_use]
121    pub fn retry_max_delay(mut self, delay: Duration) -> Self {
122        self.retry_max_delay = delay;
123        self
124    }
125
126    /// Build HTTP client
127    pub fn build(self) -> Result<Client> {
128        let mut builder = Client::builder()
129            .timeout(self.timeout)
130            .connect_timeout(self.connect_timeout)
131            .pool_max_idle_per_host(self.pool_max_idle_per_host)
132            .pool_idle_timeout(self.pool_idle_timeout)
133            .user_agent(&self.user_agent);
134
135        // reqwest 0.13 enables gzip and brotli by default
136        // To disable, use .no_gzip() and .no_brotli()
137        if !self.enable_gzip {
138            builder = builder.no_gzip();
139        }
140
141        if !self.enable_brotli {
142            builder = builder.no_brotli();
143        }
144
145        builder
146            .build()
147            .map_err(|e| Error::http_request("BUILD", "client", 0, e.to_string()))
148    }
149
150    /// Build HTTP client with retry support
151    ///
152    /// Note: Retry logic is implemented at the application level for better control.
153    /// This method returns the standard `reqwest::Client`.
154    pub fn build_with_retry(self) -> Result<Client> {
155        // Build client with configured settings
156        // Retry logic should be implemented at the application level
157        // for better control over retry behavior
158        self.build()
159    }
160}
161
162/// Create HTTP client from performance config
163#[must_use]
164pub fn create_http_client_from_config(
165    config: &crate::config::PerformanceConfig,
166) -> HttpClientBuilder {
167    HttpClientBuilder::new()
168        .timeout(Duration::from_secs(config.http_client_timeout_secs))
169        .connect_timeout(Duration::from_secs(config.http_client_connect_timeout_secs))
170        .read_timeout(Duration::from_secs(config.http_client_read_timeout_secs))
171        .pool_max_idle_per_host(config.http_client_pool_size)
172        .pool_idle_timeout(Duration::from_secs(
173            config.http_client_pool_idle_timeout_secs,
174        ))
175        .max_retries(config.http_client_max_retries)
176        .retry_initial_delay(Duration::from_millis(
177            config.http_client_retry_initial_delay_ms,
178        ))
179        .retry_max_delay(Duration::from_millis(config.http_client_retry_max_delay_ms))
180}
181
182/// Rate limiter
183pub struct RateLimiter {
184    semaphore: Arc<Semaphore>,
185    max_permits: usize,
186}
187
188impl RateLimiter {
189    /// Create a new rate limiter
190    #[must_use]
191    pub fn new(max_permits: usize) -> Self {
192        Self {
193            semaphore: Arc::new(Semaphore::new(max_permits)),
194            max_permits,
195        }
196    }
197
198    /// Acquire permit (blocks until available)
199    pub async fn acquire(&self) -> Result<tokio::sync::SemaphorePermit<'_>> {
200        self.semaphore
201            .acquire()
202            .await
203            .map_err(|e| Error::Other(format!("Failed to acquire rate limit permit: {e}")))
204    }
205
206    /// Try to acquire permit (non-blocking)
207    #[must_use]
208    pub fn try_acquire(&self) -> Option<tokio::sync::SemaphorePermit<'_>> {
209        self.semaphore.try_acquire().ok()
210    }
211
212    /// Get current number of available permits
213    #[must_use]
214    pub fn available_permits(&self) -> usize {
215        self.semaphore.available_permits()
216    }
217
218    /// Get maximum number of permits
219    #[must_use]
220    pub fn max_permits(&self) -> usize {
221        self.max_permits
222    }
223}
224
225/// Response compression utilities
226pub mod compression {
227    use crate::error::{Error, Result};
228    use flate2::write::GzEncoder;
229    use flate2::Compression;
230    use std::io::Write;
231
232    /// Compress data (Gzip)
233    pub fn gzip_compress(data: &[u8]) -> Result<Vec<u8>> {
234        let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
235        encoder
236            .write_all(data)
237            .map_err(|e| Error::Other(format!("Gzip compression failed: {e}")))?;
238        encoder
239            .finish()
240            .map_err(|e| Error::Other(format!("Gzip compression finalize failed: {e}")))
241    }
242
243    /// Decompress data (Gzip)
244    pub fn gzip_decompress(data: &[u8]) -> Result<Vec<u8>> {
245        let mut decoder = flate2::read::GzDecoder::new(data);
246        let mut decompressed = Vec::new();
247        std::io::Read::read_to_end(&mut decoder, &mut decompressed)
248            .map_err(|e| Error::Other(format!("Gzip decompression failed: {e}")))?;
249        Ok(decompressed)
250    }
251}
252
253/// String utilities
254pub mod string {
255    /// Truncate string and add ellipsis (UTF-8 safe)
256    ///
257    /// # Arguments
258    /// * `s` - The string to truncate
259    /// * `max_len` - Maximum number of characters (not bytes) to keep, including ellipsis
260    ///
261    /// # Examples
262    /// ```
263    /// use crates_docs::utils::string::truncate_with_ellipsis;
264    /// // Basic ASCII truncation
265    /// assert_eq!(truncate_with_ellipsis("hello world", 8), "hello...");
266    /// assert_eq!(truncate_with_ellipsis("short", 10), "short");
267    /// // UTF-8 safe: works with multi-byte characters
268    /// assert_eq!(truncate_with_ellipsis("你好世界", 3), "...");
269    /// assert_eq!(truncate_with_ellipsis("你好世界", 4), "你好世界"); // 4 chars <= max_len, no truncation
270    /// assert_eq!(truncate_with_ellipsis("你好世界", 5), "你好世界"); // 4 chars <= max_len, no truncation
271    /// assert_eq!(truncate_with_ellipsis("你好世界你好", 4), "你...");   // 4 chars > max_len-3, truncate
272    /// ```
273    #[must_use]
274    pub fn truncate_with_ellipsis(s: &str, max_len: usize) -> String {
275        // If max_len is 3 or less, just return ellipsis
276        if max_len <= 3 {
277            return "...".to_string();
278        }
279
280        // Collect characters to properly handle UTF-8
281        let chars: Vec<char> = s.chars().collect();
282
283        // If string is short enough, return it as-is
284        if chars.len() <= max_len {
285            return s.to_string();
286        }
287
288        // Truncate to max_len - 3 characters and add ellipsis
289        let truncated: String = chars.iter().take(max_len - 3).collect();
290        format!("{truncated}...")
291    }
292
293    /// Safely parse number
294    pub fn parse_number<T: std::str::FromStr>(s: &str, default: T) -> T {
295        s.parse().unwrap_or(default)
296    }
297
298    /// Check if string is empty or blank
299    #[must_use]
300    pub fn is_blank(s: &str) -> bool {
301        s.trim().is_empty()
302    }
303}
304
305/// Time utilities
306pub mod time {
307    use chrono::{DateTime, Utc};
308
309    /// Get current timestamp (milliseconds)
310    #[must_use]
311    pub fn current_timestamp_ms() -> i64 {
312        Utc::now().timestamp_millis()
313    }
314
315    /// Format datetime
316    #[must_use]
317    pub fn format_datetime(dt: &DateTime<Utc>) -> String {
318        dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string()
319    }
320
321    /// Calculate elapsed time (milliseconds)
322    #[must_use]
323    pub fn elapsed_ms(start: std::time::Instant) -> u128 {
324        start.elapsed().as_millis()
325    }
326}
327
328/// Validation utilities
329pub mod validation {
330    use crate::error::Error;
331
332    /// Validate crate name
333    pub fn validate_crate_name(name: &str) -> Result<(), Error> {
334        if name.is_empty() {
335            return Err(Error::Other("Crate name cannot be empty".to_string()));
336        }
337
338        if name.len() > 100 {
339            return Err(Error::Other("Crate name is too long".to_string()));
340        }
341
342        // Basic validation: only allow letters, digits, underscores, hyphens
343        if !name
344            .chars()
345            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
346        {
347            return Err(Error::Other(
348                "Crate name contains invalid characters".to_string(),
349            ));
350        }
351
352        Ok(())
353    }
354
355    /// Validate version number
356    pub fn validate_version(version: &str) -> Result<(), Error> {
357        if version.is_empty() {
358            return Err(Error::Other("Version cannot be empty".to_string()));
359        }
360
361        if version.len() > 50 {
362            return Err(Error::Other("Version is too long".to_string()));
363        }
364
365        // Simple validation: should contain digits and dots
366        if !version.chars().any(|c| c.is_ascii_digit()) {
367            return Err(Error::Other("Version must contain digits".to_string()));
368        }
369
370        Ok(())
371    }
372
373    /// Validate search query
374    pub fn validate_search_query(query: &str) -> Result<(), Error> {
375        if query.is_empty() {
376            return Err(Error::Other("Search query cannot be empty".to_string()));
377        }
378
379        if query.len() > 200 {
380            return Err(Error::Other("Search query is too long".to_string()));
381        }
382
383        Ok(())
384    }
385}
386
387/// Performance monitoring
388pub mod metrics {
389    use std::sync::atomic::{AtomicU64, Ordering};
390    use std::sync::Arc;
391    use std::time::Instant;
392
393    /// Performance counter
394    #[derive(Clone)]
395    pub struct PerformanceCounter {
396        total_requests: Arc<AtomicU64>,
397        successful_requests: Arc<AtomicU64>,
398        failed_requests: Arc<AtomicU64>,
399        total_response_time_ms: Arc<AtomicU64>,
400    }
401
402    impl PerformanceCounter {
403        /// Create a new performance counter
404        #[must_use]
405        pub fn new() -> Self {
406            Self {
407                total_requests: Arc::new(AtomicU64::new(0)),
408                successful_requests: Arc::new(AtomicU64::new(0)),
409                failed_requests: Arc::new(AtomicU64::new(0)),
410                total_response_time_ms: Arc::new(AtomicU64::new(0)),
411            }
412        }
413
414        /// Record request start
415        #[must_use]
416        pub fn record_request_start(&self) -> Instant {
417            self.total_requests.fetch_add(1, Ordering::Relaxed);
418            Instant::now()
419        }
420
421        /// Record request completion
422        #[allow(clippy::cast_possible_truncation)]
423        pub fn record_request_complete(&self, start: Instant, success: bool) {
424            let duration_ms = start.elapsed().as_millis() as u64;
425            self.total_response_time_ms
426                .fetch_add(duration_ms, Ordering::Relaxed);
427
428            if success {
429                self.successful_requests.fetch_add(1, Ordering::Relaxed);
430            } else {
431                self.failed_requests.fetch_add(1, Ordering::Relaxed);
432            }
433        }
434
435        /// Get statistics
436        #[must_use]
437        pub fn get_stats(&self) -> PerformanceStats {
438            let total = self.total_requests.load(Ordering::Relaxed);
439            let success = self.successful_requests.load(Ordering::Relaxed);
440            let failed = self.failed_requests.load(Ordering::Relaxed);
441            let total_time = self.total_response_time_ms.load(Ordering::Relaxed);
442
443            #[allow(clippy::cast_precision_loss)]
444            let avg_response_time = if total > 0 {
445                total_time as f64 / total as f64
446            } else {
447                0.0
448            };
449
450            #[allow(clippy::cast_precision_loss)]
451            let success_rate = if total > 0 {
452                success as f64 / total as f64 * 100.0
453            } else {
454                0.0
455            };
456
457            PerformanceStats {
458                total_requests: total,
459                successful_requests: success,
460                failed_requests: failed,
461                average_response_time_ms: avg_response_time,
462                success_rate_percent: success_rate,
463            }
464        }
465
466        /// Reset counter
467        pub fn reset(&self) {
468            self.total_requests.store(0, Ordering::Relaxed);
469            self.successful_requests.store(0, Ordering::Relaxed);
470            self.failed_requests.store(0, Ordering::Relaxed);
471            self.total_response_time_ms.store(0, Ordering::Relaxed);
472        }
473    }
474
475    impl Default for PerformanceCounter {
476        fn default() -> Self {
477            Self::new()
478        }
479    }
480
481    /// Performance statistics
482    #[derive(Debug, Clone, serde::Serialize)]
483    pub struct PerformanceStats {
484        /// Total requests
485        pub total_requests: u64,
486        /// Successful requests
487        pub successful_requests: u64,
488        /// Failed requests
489        pub failed_requests: u64,
490        /// Average response time (milliseconds)
491        pub average_response_time_ms: f64,
492        /// Success rate (percentage)
493        pub success_rate_percent: f64,
494    }
495}