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]
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 pub fn parse_number<T: std::str::FromStr>(s: &str, default: T) -> T {
194 s.parse().unwrap_or(default)
195 }
196
197 #[must_use]
199 pub fn is_blank(s: &str) -> bool {
200 s.trim().is_empty()
201 }
202}
203
204pub mod time {
206 use chrono::{DateTime, Utc};
207
208 #[must_use]
210 pub fn current_timestamp_ms() -> i64 {
211 Utc::now().timestamp_millis()
212 }
213
214 #[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 #[must_use]
222 pub fn elapsed_ms(start: std::time::Instant) -> u128 {
223 start.elapsed().as_millis()
224 }
225}
226
227pub mod validation {
229 use crate::error::Error;
230
231 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 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 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 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 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
286pub mod metrics {
288 use std::sync::atomic::{AtomicU64, Ordering};
289 use std::sync::Arc;
290 use std::time::Instant;
291
292 #[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 #[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 #[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 #[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 #[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 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 #[derive(Debug, Clone, serde::Serialize)]
382 pub struct PerformanceStats {
383 pub total_requests: u64,
385 pub successful_requests: u64,
387 pub failed_requests: u64,
389 pub average_response_time_ms: f64,
391 pub success_rate_percent: f64,
393 }
394}