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 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 #[must_use]
35 pub fn new() -> Self {
36 Self::default()
37 }
38
39 #[must_use]
41 pub fn timeout(mut self, timeout: Duration) -> Self {
42 self.timeout = timeout;
43 self
44 }
45
46 #[must_use]
48 pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
49 self.connect_timeout = connect_timeout;
50 self
51 }
52
53 #[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 #[must_use]
62 pub fn user_agent(mut self, user_agent: String) -> Self {
63 self.user_agent = user_agent;
64 self
65 }
66
67 #[must_use]
69 pub fn enable_gzip(mut self, enable: bool) -> Self {
70 self.enable_gzip = enable;
71 self
72 }
73
74 #[must_use]
76 pub fn enable_brotli(mut self, enable: bool) -> Self {
77 self.enable_brotli = enable;
78 self
79 }
80
81 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 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
105pub struct RateLimiter {
107 semaphore: Arc<Semaphore>,
108 max_permits: usize,
109}
110
111impl RateLimiter {
112 #[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 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 #[must_use]
131 pub fn try_acquire(&self) -> Option<tokio::sync::SemaphorePermit<'_>> {
132 self.semaphore.try_acquire().ok()
133 }
134
135 #[must_use]
137 pub fn available_permits(&self) -> usize {
138 self.semaphore.available_permits()
139 }
140
141 #[must_use]
143 pub fn max_permits(&self) -> usize {
144 self.max_permits
145 }
146}
147
148pub mod compression {
150 use crate::error::{Error, Result};
151 use flate2::write::GzEncoder;
152 use flate2::Compression;
153 use std::io::Write;
154
155 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 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
176pub mod string {
178 #[must_use]
197 pub fn truncate_with_ellipsis(s: &str, max_len: usize) -> String {
198 if max_len <= 3 {
200 return "...".to_string();
201 }
202
203 let chars: Vec<char> = s.chars().collect();
205
206 if chars.len() <= max_len {
208 return s.to_string();
209 }
210
211 let truncated: String = chars.iter().take(max_len - 3).collect();
213 format!("{truncated}...")
214 }
215
216 pub fn parse_number<T: std::str::FromStr>(s: &str, default: T) -> T {
218 s.parse().unwrap_or(default)
219 }
220
221 #[must_use]
223 pub fn is_blank(s: &str) -> bool {
224 s.trim().is_empty()
225 }
226}
227
228pub mod time {
230 use chrono::{DateTime, Utc};
231
232 #[must_use]
234 pub fn current_timestamp_ms() -> i64 {
235 Utc::now().timestamp_millis()
236 }
237
238 #[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 #[must_use]
246 pub fn elapsed_ms(start: std::time::Instant) -> u128 {
247 start.elapsed().as_millis()
248 }
249}
250
251pub mod validation {
253 use crate::error::Error;
254
255 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 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 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 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 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
310pub mod metrics {
312 use std::sync::atomic::{AtomicU64, Ordering};
313 use std::sync::Arc;
314 use std::time::Instant;
315
316 #[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 #[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 #[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 #[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 #[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 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 #[derive(Debug, Clone, serde::Serialize)]
406 pub struct PerformanceStats {
407 pub total_requests: u64,
409 pub successful_requests: u64,
411 pub failed_requests: u64,
413 pub average_response_time_ms: f64,
415 pub success_rate_percent: f64,
417 }
418}