1use crate::error::{Error, Result};
4use reqwest::Client;
5use std::sync::Arc;
6use std::time::Duration;
7use tokio::sync::Semaphore;
8
9pub 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 #[must_use]
45 pub fn new() -> Self {
46 Self::default()
47 }
48
49 #[must_use]
51 pub fn timeout(mut self, timeout: Duration) -> Self {
52 self.timeout = timeout;
53 self
54 }
55
56 #[must_use]
58 pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
59 self.connect_timeout = connect_timeout;
60 self
61 }
62
63 #[must_use]
65 pub fn read_timeout(mut self, read_timeout: Duration) -> Self {
66 self.read_timeout = read_timeout;
67 self
68 }
69
70 #[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 #[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 #[must_use]
86 pub fn user_agent(mut self, user_agent: String) -> Self {
87 self.user_agent = user_agent;
88 self
89 }
90
91 #[must_use]
93 pub fn enable_gzip(mut self, enable: bool) -> Self {
94 self.enable_gzip = enable;
95 self
96 }
97
98 #[must_use]
100 pub fn enable_brotli(mut self, enable: bool) -> Self {
101 self.enable_brotli = enable;
102 self
103 }
104
105 #[must_use]
107 pub fn max_retries(mut self, max_retries: u32) -> Self {
108 self.max_retries = max_retries;
109 self
110 }
111
112 #[must_use]
114 pub fn retry_initial_delay(mut self, delay: Duration) -> Self {
115 self.retry_initial_delay = delay;
116 self
117 }
118
119 #[must_use]
121 pub fn retry_max_delay(mut self, delay: Duration) -> Self {
122 self.retry_max_delay = delay;
123 self
124 }
125
126 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 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 pub fn build_with_retry(self) -> Result<Client> {
155 self.build()
159 }
160}
161
162#[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
182pub struct RateLimiter {
184 semaphore: Arc<Semaphore>,
185 max_permits: usize,
186}
187
188impl RateLimiter {
189 #[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 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 #[must_use]
208 pub fn try_acquire(&self) -> Option<tokio::sync::SemaphorePermit<'_>> {
209 self.semaphore.try_acquire().ok()
210 }
211
212 #[must_use]
214 pub fn available_permits(&self) -> usize {
215 self.semaphore.available_permits()
216 }
217
218 #[must_use]
220 pub fn max_permits(&self) -> usize {
221 self.max_permits
222 }
223}
224
225pub mod compression {
227 use crate::error::{Error, Result};
228 use flate2::write::GzEncoder;
229 use flate2::Compression;
230 use std::io::Write;
231
232 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 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
253pub mod string {
255 #[must_use]
274 pub fn truncate_with_ellipsis(s: &str, max_len: usize) -> String {
275 if max_len <= 3 {
277 return "...".to_string();
278 }
279
280 let chars: Vec<char> = s.chars().collect();
282
283 if chars.len() <= max_len {
285 return s.to_string();
286 }
287
288 let truncated: String = chars.iter().take(max_len - 3).collect();
290 format!("{truncated}...")
291 }
292
293 pub fn parse_number<T: std::str::FromStr>(s: &str, default: T) -> T {
295 s.parse().unwrap_or(default)
296 }
297
298 #[must_use]
300 pub fn is_blank(s: &str) -> bool {
301 s.trim().is_empty()
302 }
303}
304
305pub mod time {
307 use chrono::{DateTime, Utc};
308
309 #[must_use]
311 pub fn current_timestamp_ms() -> i64 {
312 Utc::now().timestamp_millis()
313 }
314
315 #[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 #[must_use]
323 pub fn elapsed_ms(start: std::time::Instant) -> u128 {
324 start.elapsed().as_millis()
325 }
326}
327
328pub mod validation {
330 use crate::error::Error;
331
332 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 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 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 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 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
387pub mod metrics {
389 use std::sync::atomic::{AtomicU64, Ordering};
390 use std::sync::Arc;
391 use std::time::Instant;
392
393 #[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 #[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 #[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 #[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 #[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 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 #[derive(Debug, Clone, serde::Serialize)]
483 pub struct PerformanceStats {
484 pub total_requests: u64,
486 pub successful_requests: u64,
488 pub failed_requests: u64,
490 pub average_response_time_ms: f64,
492 pub success_rate_percent: f64,
494 }
495}