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