1use crate::error::{Error, Result};
4use reqwest::Client;
5use reqwest_middleware::ClientBuilder;
6use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
7use std::sync::Arc;
8use std::time::Duration;
9use tokio::sync::Semaphore;
10
11pub struct HttpClientBuilder {
16 timeout: Duration,
17 connect_timeout: Duration,
18 read_timeout: Duration,
19 pool_max_idle_per_host: usize,
20 pool_idle_timeout: Duration,
21 user_agent: String,
22 enable_gzip: bool,
23 enable_brotli: bool,
24 max_retries: u32,
25 retry_initial_delay: Duration,
26 retry_max_delay: Duration,
27}
28
29impl Default for HttpClientBuilder {
30 fn default() -> Self {
31 Self {
32 timeout: Duration::from_secs(30),
33 connect_timeout: Duration::from_secs(10),
34 read_timeout: Duration::from_secs(30),
35 pool_max_idle_per_host: 10,
36 pool_idle_timeout: Duration::from_secs(90),
37 user_agent: format!("CratesDocsMCP/{}", crate::VERSION),
38 enable_gzip: true,
39 enable_brotli: true,
40 max_retries: 3,
41 retry_initial_delay: Duration::from_millis(100),
42 retry_max_delay: Duration::from_secs(10),
43 }
44 }
45}
46
47impl HttpClientBuilder {
48 #[must_use]
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 #[must_use]
56 pub fn timeout(mut self, timeout: Duration) -> Self {
57 self.timeout = timeout;
58 self
59 }
60
61 #[must_use]
63 pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
64 self.connect_timeout = connect_timeout;
65 self
66 }
67
68 #[must_use]
70 pub fn read_timeout(mut self, read_timeout: Duration) -> Self {
71 self.read_timeout = read_timeout;
72 self
73 }
74
75 #[must_use]
77 pub fn pool_max_idle_per_host(mut self, max_idle: usize) -> Self {
78 self.pool_max_idle_per_host = max_idle;
79 self
80 }
81
82 #[must_use]
84 pub fn pool_idle_timeout(mut self, idle_timeout: Duration) -> Self {
85 self.pool_idle_timeout = idle_timeout;
86 self
87 }
88
89 #[must_use]
91 pub fn user_agent(mut self, user_agent: String) -> Self {
92 self.user_agent = user_agent;
93 self
94 }
95
96 #[must_use]
98 pub fn enable_gzip(mut self, enable: bool) -> Self {
99 self.enable_gzip = enable;
100 self
101 }
102
103 #[must_use]
105 pub fn enable_brotli(mut self, enable: bool) -> Self {
106 self.enable_brotli = enable;
107 self
108 }
109
110 #[must_use]
112 pub fn max_retries(mut self, max_retries: u32) -> Self {
113 self.max_retries = max_retries;
114 self
115 }
116
117 #[must_use]
119 pub fn retry_initial_delay(mut self, delay: Duration) -> Self {
120 self.retry_initial_delay = delay;
121 self
122 }
123
124 #[must_use]
126 pub fn retry_max_delay(mut self, delay: Duration) -> Self {
127 self.retry_max_delay = delay;
128 self
129 }
130
131 pub fn build(self) -> Result<reqwest_middleware::ClientWithMiddleware> {
141 let mut builder = Client::builder()
142 .timeout(self.timeout)
143 .connect_timeout(self.connect_timeout)
144 .pool_max_idle_per_host(self.pool_max_idle_per_host)
145 .pool_idle_timeout(self.pool_idle_timeout)
146 .user_agent(&self.user_agent);
147
148 if !self.enable_gzip {
151 builder = builder.no_gzip();
152 }
153
154 if !self.enable_brotli {
155 builder = builder.no_brotli();
156 }
157
158 let client = builder
159 .build()
160 .map_err(|e| Error::http_request("BUILD", "client", 0, e.to_string()))?;
161
162 let retry_policy = ExponentialBackoff::builder()
164 .retry_bounds(self.retry_initial_delay, self.retry_max_delay)
165 .build_with_max_retries(self.max_retries);
166
167 Ok(ClientBuilder::new(client)
169 .with(RetryTransientMiddleware::new_with_policy(retry_policy))
170 .build())
171 }
172
173 pub fn build_plain(self) -> Result<Client> {
178 let mut builder = Client::builder()
179 .timeout(self.timeout)
180 .connect_timeout(self.connect_timeout)
181 .pool_max_idle_per_host(self.pool_max_idle_per_host)
182 .pool_idle_timeout(self.pool_idle_timeout)
183 .user_agent(&self.user_agent);
184
185 if !self.enable_gzip {
186 builder = builder.no_gzip();
187 }
188
189 if !self.enable_brotli {
190 builder = builder.no_brotli();
191 }
192
193 builder
194 .build()
195 .map_err(|e| Error::http_request("BUILD", "client", 0, e.to_string()))
196 }
197}
198
199#[must_use]
205pub fn create_http_client_from_config(
206 config: &crate::config::PerformanceConfig,
207) -> HttpClientBuilder {
208 HttpClientBuilder::new()
209 .timeout(Duration::from_secs(config.http_client_timeout_secs))
210 .connect_timeout(Duration::from_secs(config.http_client_connect_timeout_secs))
211 .read_timeout(Duration::from_secs(config.http_client_read_timeout_secs))
212 .pool_max_idle_per_host(config.http_client_pool_size)
213 .pool_idle_timeout(Duration::from_secs(
214 config.http_client_pool_idle_timeout_secs,
215 ))
216 .max_retries(config.http_client_max_retries)
217 .retry_initial_delay(Duration::from_millis(
218 config.http_client_retry_initial_delay_ms,
219 ))
220 .retry_max_delay(Duration::from_millis(config.http_client_retry_max_delay_ms))
221}
222
223pub struct RateLimiter {
225 semaphore: Arc<Semaphore>,
226 max_permits: usize,
227}
228
229impl RateLimiter {
230 #[must_use]
232 pub fn new(max_permits: usize) -> Self {
233 Self {
234 semaphore: Arc::new(Semaphore::new(max_permits)),
235 max_permits,
236 }
237 }
238
239 pub async fn acquire(&self) -> Result<tokio::sync::SemaphorePermit<'_>> {
241 self.semaphore
242 .acquire()
243 .await
244 .map_err(|e| Error::Other(format!("Failed to acquire rate limit permit: {e}")))
245 }
246
247 #[must_use]
249 pub fn try_acquire(&self) -> Option<tokio::sync::SemaphorePermit<'_>> {
250 self.semaphore.try_acquire().ok()
251 }
252
253 #[must_use]
255 pub fn available_permits(&self) -> usize {
256 self.semaphore.available_permits()
257 }
258
259 #[must_use]
261 pub fn max_permits(&self) -> usize {
262 self.max_permits
263 }
264}
265
266pub mod compression {
268 use crate::error::{Error, Result};
269 use flate2::write::GzEncoder;
270 use flate2::Compression;
271 use std::io::Write;
272
273 pub fn gzip_compress(data: &[u8]) -> Result<Vec<u8>> {
275 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
276 encoder
277 .write_all(data)
278 .map_err(|e| Error::Other(format!("Gzip compression failed: {e}")))?;
279 encoder
280 .finish()
281 .map_err(|e| Error::Other(format!("Gzip compression finalize failed: {e}")))
282 }
283
284 pub fn gzip_decompress(data: &[u8]) -> Result<Vec<u8>> {
286 let mut decoder = flate2::read::GzDecoder::new(data);
287 let mut decompressed = Vec::new();
288 std::io::Read::read_to_end(&mut decoder, &mut decompressed)
289 .map_err(|e| Error::Other(format!("Gzip decompression failed: {e}")))?;
290 Ok(decompressed)
291 }
292}
293
294pub mod string {
296 #[must_use]
315 pub fn truncate_with_ellipsis(s: &str, max_len: usize) -> String {
316 if max_len <= 3 {
318 return "...".to_string();
319 }
320
321 let chars: Vec<char> = s.chars().collect();
323
324 if chars.len() <= max_len {
326 return s.to_string();
327 }
328
329 let truncated: String = chars.iter().take(max_len - 3).collect();
331 format!("{truncated}...")
332 }
333
334 pub fn parse_number<T: std::str::FromStr>(s: &str, default: T) -> T {
336 s.parse().unwrap_or(default)
337 }
338
339 #[must_use]
341 pub fn is_blank(s: &str) -> bool {
342 s.trim().is_empty()
343 }
344}
345
346pub mod time {
348 use chrono::{DateTime, Utc};
349
350 #[must_use]
352 pub fn current_timestamp_ms() -> i64 {
353 Utc::now().timestamp_millis()
354 }
355
356 #[must_use]
358 pub fn format_datetime(dt: &DateTime<Utc>) -> String {
359 dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string()
360 }
361
362 #[must_use]
364 pub fn elapsed_ms(start: std::time::Instant) -> u128 {
365 start.elapsed().as_millis()
366 }
367}
368
369pub mod validation {
371 use crate::error::Error;
372
373 pub fn validate_crate_name(name: &str) -> Result<(), Error> {
375 if name.is_empty() {
376 return Err(Error::Other("Crate name cannot be empty".to_string()));
377 }
378
379 if name.len() > 100 {
380 return Err(Error::Other("Crate name is too long".to_string()));
381 }
382
383 if !name
385 .chars()
386 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
387 {
388 return Err(Error::Other(
389 "Crate name contains invalid characters".to_string(),
390 ));
391 }
392
393 Ok(())
394 }
395
396 pub fn validate_version(version: &str) -> Result<(), Error> {
398 if version.is_empty() {
399 return Err(Error::Other("Version cannot be empty".to_string()));
400 }
401
402 if version.len() > 50 {
403 return Err(Error::Other("Version is too long".to_string()));
404 }
405
406 if !version.chars().any(|c| c.is_ascii_digit()) {
408 return Err(Error::Other("Version must contain digits".to_string()));
409 }
410
411 Ok(())
412 }
413
414 pub fn validate_search_query(query: &str) -> Result<(), Error> {
416 if query.is_empty() {
417 return Err(Error::Other("Search query cannot be empty".to_string()));
418 }
419
420 if query.len() > 200 {
421 return Err(Error::Other("Search query is too long".to_string()));
422 }
423
424 Ok(())
425 }
426}
427
428pub mod metrics {
430 use std::sync::atomic::{AtomicU64, Ordering};
431 use std::sync::Arc;
432 use std::time::Instant;
433
434 #[derive(Clone)]
436 pub struct PerformanceCounter {
437 total_requests: Arc<AtomicU64>,
438 successful_requests: Arc<AtomicU64>,
439 failed_requests: Arc<AtomicU64>,
440 total_response_time_ms: Arc<AtomicU64>,
441 }
442
443 impl PerformanceCounter {
444 #[must_use]
446 pub fn new() -> Self {
447 Self {
448 total_requests: Arc::new(AtomicU64::new(0)),
449 successful_requests: Arc::new(AtomicU64::new(0)),
450 failed_requests: Arc::new(AtomicU64::new(0)),
451 total_response_time_ms: Arc::new(AtomicU64::new(0)),
452 }
453 }
454
455 #[must_use]
457 pub fn record_request_start(&self) -> Instant {
458 self.total_requests.fetch_add(1, Ordering::Relaxed);
459 Instant::now()
460 }
461
462 #[allow(clippy::cast_possible_truncation)]
464 pub fn record_request_complete(&self, start: Instant, success: bool) {
465 let duration_ms = start.elapsed().as_millis() as u64;
466 self.total_response_time_ms
467 .fetch_add(duration_ms, Ordering::Relaxed);
468
469 if success {
470 self.successful_requests.fetch_add(1, Ordering::Relaxed);
471 } else {
472 self.failed_requests.fetch_add(1, Ordering::Relaxed);
473 }
474 }
475
476 #[must_use]
478 pub fn get_stats(&self) -> PerformanceStats {
479 let total = self.total_requests.load(Ordering::Relaxed);
480 let success = self.successful_requests.load(Ordering::Relaxed);
481 let failed = self.failed_requests.load(Ordering::Relaxed);
482 let total_time = self.total_response_time_ms.load(Ordering::Relaxed);
483
484 #[allow(clippy::cast_precision_loss)]
485 let avg_response_time = if total > 0 {
486 total_time as f64 / total as f64
487 } else {
488 0.0
489 };
490
491 #[allow(clippy::cast_precision_loss)]
492 let success_rate = if total > 0 {
493 success as f64 / total as f64 * 100.0
494 } else {
495 0.0
496 };
497
498 PerformanceStats {
499 total_requests: total,
500 successful_requests: success,
501 failed_requests: failed,
502 average_response_time_ms: avg_response_time,
503 success_rate_percent: success_rate,
504 }
505 }
506
507 pub fn reset(&self) {
509 self.total_requests.store(0, Ordering::Relaxed);
510 self.successful_requests.store(0, Ordering::Relaxed);
511 self.failed_requests.store(0, Ordering::Relaxed);
512 self.total_response_time_ms.store(0, Ordering::Relaxed);
513 }
514 }
515
516 impl Default for PerformanceCounter {
517 fn default() -> Self {
518 Self::new()
519 }
520 }
521
522 #[derive(Debug, Clone, serde::Serialize)]
524 pub struct PerformanceStats {
525 pub total_requests: u64,
527 pub successful_requests: u64,
529 pub failed_requests: u64,
531 pub average_response_time_ms: f64,
533 pub success_rate_percent: f64,
535 }
536}