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
179    #[must_use]
180    pub fn truncate_with_ellipsis(s: &str, max_len: usize) -> String {
181        if s.len() <= max_len {
182            return s.to_string();
183        }
184
185        if max_len <= 3 {
186            return "...".to_string();
187        }
188
189        format!("{}...", &s[..max_len - 3])
190    }
191
192    /// Safely parse number
193    pub fn parse_number<T: std::str::FromStr>(s: &str, default: T) -> T {
194        s.parse().unwrap_or(default)
195    }
196
197    /// Check if string is empty or blank
198    #[must_use]
199    pub fn is_blank(s: &str) -> bool {
200        s.trim().is_empty()
201    }
202}
203
204/// Time utilities
205pub mod time {
206    use chrono::{DateTime, Utc};
207
208    /// Get current timestamp (milliseconds)
209    #[must_use]
210    pub fn current_timestamp_ms() -> i64 {
211        Utc::now().timestamp_millis()
212    }
213
214    /// Format datetime
215    #[must_use]
216    pub fn format_datetime(dt: &DateTime<Utc>) -> String {
217        dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string()
218    }
219
220    /// Calculate elapsed time (milliseconds)
221    #[must_use]
222    pub fn elapsed_ms(start: std::time::Instant) -> u128 {
223        start.elapsed().as_millis()
224    }
225}
226
227/// Validation utilities
228pub mod validation {
229    use crate::error::Error;
230
231    /// Validate crate name
232    pub fn validate_crate_name(name: &str) -> Result<(), Error> {
233        if name.is_empty() {
234            return Err(Error::Other("Crate name cannot be empty".to_string()));
235        }
236
237        if name.len() > 100 {
238            return Err(Error::Other("Crate name is too long".to_string()));
239        }
240
241        // Basic validation: only allow letters, digits, underscores, hyphens
242        if !name
243            .chars()
244            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
245        {
246            return Err(Error::Other(
247                "Crate name contains invalid characters".to_string(),
248            ));
249        }
250
251        Ok(())
252    }
253
254    /// Validate version number
255    pub fn validate_version(version: &str) -> Result<(), Error> {
256        if version.is_empty() {
257            return Err(Error::Other("Version cannot be empty".to_string()));
258        }
259
260        if version.len() > 50 {
261            return Err(Error::Other("Version is too long".to_string()));
262        }
263
264        // Simple validation: should contain digits and dots
265        if !version.chars().any(|c| c.is_ascii_digit()) {
266            return Err(Error::Other("Version must contain digits".to_string()));
267        }
268
269        Ok(())
270    }
271
272    /// Validate search query
273    pub fn validate_search_query(query: &str) -> Result<(), Error> {
274        if query.is_empty() {
275            return Err(Error::Other("Search query cannot be empty".to_string()));
276        }
277
278        if query.len() > 200 {
279            return Err(Error::Other("Search query is too long".to_string()));
280        }
281
282        Ok(())
283    }
284}
285
286/// Performance monitoring
287pub mod metrics {
288    use std::sync::atomic::{AtomicU64, Ordering};
289    use std::sync::Arc;
290    use std::time::Instant;
291
292    /// Performance counter
293    #[derive(Clone)]
294    pub struct PerformanceCounter {
295        total_requests: Arc<AtomicU64>,
296        successful_requests: Arc<AtomicU64>,
297        failed_requests: Arc<AtomicU64>,
298        total_response_time_ms: Arc<AtomicU64>,
299    }
300
301    impl PerformanceCounter {
302        /// Create a new performance counter
303        #[must_use]
304        pub fn new() -> Self {
305            Self {
306                total_requests: Arc::new(AtomicU64::new(0)),
307                successful_requests: Arc::new(AtomicU64::new(0)),
308                failed_requests: Arc::new(AtomicU64::new(0)),
309                total_response_time_ms: Arc::new(AtomicU64::new(0)),
310            }
311        }
312
313        /// Record request start
314        #[must_use]
315        pub fn record_request_start(&self) -> Instant {
316            self.total_requests.fetch_add(1, Ordering::Relaxed);
317            Instant::now()
318        }
319
320        /// Record request completion
321        #[allow(clippy::cast_possible_truncation)]
322        pub fn record_request_complete(&self, start: Instant, success: bool) {
323            let duration_ms = start.elapsed().as_millis() as u64;
324            self.total_response_time_ms
325                .fetch_add(duration_ms, Ordering::Relaxed);
326
327            if success {
328                self.successful_requests.fetch_add(1, Ordering::Relaxed);
329            } else {
330                self.failed_requests.fetch_add(1, Ordering::Relaxed);
331            }
332        }
333
334        /// Get statistics
335        #[must_use]
336        pub fn get_stats(&self) -> PerformanceStats {
337            let total = self.total_requests.load(Ordering::Relaxed);
338            let success = self.successful_requests.load(Ordering::Relaxed);
339            let failed = self.failed_requests.load(Ordering::Relaxed);
340            let total_time = self.total_response_time_ms.load(Ordering::Relaxed);
341
342            #[allow(clippy::cast_precision_loss)]
343            let avg_response_time = if total > 0 {
344                total_time as f64 / total as f64
345            } else {
346                0.0
347            };
348
349            #[allow(clippy::cast_precision_loss)]
350            let success_rate = if total > 0 {
351                success as f64 / total as f64 * 100.0
352            } else {
353                0.0
354            };
355
356            PerformanceStats {
357                total_requests: total,
358                successful_requests: success,
359                failed_requests: failed,
360                average_response_time_ms: avg_response_time,
361                success_rate_percent: success_rate,
362            }
363        }
364
365        /// Reset counter
366        pub fn reset(&self) {
367            self.total_requests.store(0, Ordering::Relaxed);
368            self.successful_requests.store(0, Ordering::Relaxed);
369            self.failed_requests.store(0, Ordering::Relaxed);
370            self.total_response_time_ms.store(0, Ordering::Relaxed);
371        }
372    }
373
374    impl Default for PerformanceCounter {
375        fn default() -> Self {
376            Self::new()
377        }
378    }
379
380    /// Performance statistics
381    #[derive(Debug, Clone, serde::Serialize)]
382    pub struct PerformanceStats {
383        /// Total requests
384        pub total_requests: u64,
385        /// Successful requests
386        pub successful_requests: u64,
387        /// Failed requests
388        pub failed_requests: u64,
389        /// Average response time (milliseconds)
390        pub average_response_time_ms: f64,
391        /// Success rate (percentage)
392        pub success_rate_percent: f64,
393    }
394}