use crate::error::{Error, Result};
use reqwest::Client;
use reqwest_middleware::ClientBuilder;
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use tokio::sync::Semaphore;
static GLOBAL_HTTP_CLIENT: OnceLock<Arc<reqwest_middleware::ClientWithMiddleware>> =
OnceLock::new();
static INIT_ERROR: OnceLock<String> = OnceLock::new();
pub fn init_global_http_client(config: &crate::config::PerformanceConfig) -> Result<()> {
if GLOBAL_HTTP_CLIENT.get().is_some() {
return Ok(());
}
if let Some(err_msg) = INIT_ERROR.get() {
return Err(Error::initialization(
"global_http_client",
format!("Previous initialization failed: {err_msg}"),
));
}
let client_result = create_http_client_from_config(config).build();
match client_result {
Ok(client) => {
let client_arc = Arc::new(client);
let _ = GLOBAL_HTTP_CLIENT.set(client_arc);
Ok(())
}
Err(e) => {
let err_msg = format!("Failed to create global HTTP client: {e}");
let _ = INIT_ERROR.set(err_msg.clone());
Err(Error::initialization("global_http_client", err_msg))
}
}
}
#[must_use = "returns a Result that should be checked"]
pub fn get_global_http_client() -> Result<Arc<reqwest_middleware::ClientWithMiddleware>> {
GLOBAL_HTTP_CLIENT.get().cloned().ok_or_else(|| {
Error::initialization(
"global_http_client",
"Global HTTP client not initialized. Call init_global_http_client() first.",
)
})
}
pub fn get_or_init_global_http_client() -> Result<Arc<reqwest_middleware::ClientWithMiddleware>> {
if let Some(client) = GLOBAL_HTTP_CLIENT.get() {
return Ok(client.clone());
}
let default_config = crate::config::PerformanceConfig::default();
init_global_http_client(&default_config)?;
GLOBAL_HTTP_CLIENT.get().cloned().ok_or_else(|| {
Error::initialization(
"global_http_client",
"HTTP client initialization failed unexpectedly".to_string(),
)
})
}
pub struct HttpClientBuilder {
timeout: Duration,
connect_timeout: Duration,
read_timeout: Duration,
pool_max_idle_per_host: usize,
pool_idle_timeout: Duration,
user_agent: String,
enable_gzip: bool,
enable_brotli: bool,
max_retries: u32,
retry_initial_delay: Duration,
retry_max_delay: Duration,
}
impl Default for HttpClientBuilder {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
connect_timeout: Duration::from_secs(10),
read_timeout: Duration::from_secs(30),
pool_max_idle_per_host: 10,
pool_idle_timeout: Duration::from_secs(90),
user_agent: format!("CratesDocsMCP/{}", crate::VERSION),
enable_gzip: true,
enable_brotli: true,
max_retries: 3,
retry_initial_delay: Duration::from_millis(100),
retry_max_delay: Duration::from_secs(10),
}
}
}
impl HttpClientBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
self.connect_timeout = connect_timeout;
self
}
#[must_use]
pub fn read_timeout(mut self, read_timeout: Duration) -> Self {
self.read_timeout = read_timeout;
self
}
#[must_use]
pub fn pool_max_idle_per_host(mut self, max_idle: usize) -> Self {
self.pool_max_idle_per_host = max_idle;
self
}
#[must_use]
pub fn pool_idle_timeout(mut self, idle_timeout: Duration) -> Self {
self.pool_idle_timeout = idle_timeout;
self
}
#[must_use]
pub fn user_agent(mut self, user_agent: String) -> Self {
self.user_agent = user_agent;
self
}
#[must_use]
pub fn enable_gzip(mut self, enable: bool) -> Self {
self.enable_gzip = enable;
self
}
#[must_use]
pub fn enable_brotli(mut self, enable: bool) -> Self {
self.enable_brotli = enable;
self
}
#[must_use]
pub fn max_retries(mut self, max_retries: u32) -> Self {
self.max_retries = max_retries;
self
}
#[must_use]
pub fn retry_initial_delay(mut self, delay: Duration) -> Self {
self.retry_initial_delay = delay;
self
}
#[must_use]
pub fn retry_max_delay(mut self, delay: Duration) -> Self {
self.retry_max_delay = delay;
self
}
pub fn build(self) -> Result<reqwest_middleware::ClientWithMiddleware> {
let mut builder = Client::builder()
.timeout(self.timeout)
.connect_timeout(self.connect_timeout)
.pool_max_idle_per_host(self.pool_max_idle_per_host)
.pool_idle_timeout(self.pool_idle_timeout)
.user_agent(&self.user_agent);
if !self.enable_gzip {
builder = builder.no_gzip();
}
if !self.enable_brotli {
builder = builder.no_brotli();
}
let client = builder
.build()
.map_err(|e| Error::http_request("BUILD", "client", 0, e.to_string()))?;
let retry_policy = ExponentialBackoff::builder()
.retry_bounds(self.retry_initial_delay, self.retry_max_delay)
.build_with_max_retries(self.max_retries);
Ok(ClientBuilder::new(client)
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build())
}
pub fn build_plain(self) -> Result<Client> {
let mut builder = Client::builder()
.timeout(self.timeout)
.connect_timeout(self.connect_timeout)
.pool_max_idle_per_host(self.pool_max_idle_per_host)
.pool_idle_timeout(self.pool_idle_timeout)
.user_agent(&self.user_agent);
if !self.enable_gzip {
builder = builder.no_gzip();
}
if !self.enable_brotli {
builder = builder.no_brotli();
}
builder
.build()
.map_err(|e| Error::http_request("BUILD", "client", 0, e.to_string()))
}
}
#[must_use]
pub fn create_http_client_from_config(
config: &crate::config::PerformanceConfig,
) -> HttpClientBuilder {
HttpClientBuilder::new()
.timeout(Duration::from_secs(config.http_client_timeout_secs))
.connect_timeout(Duration::from_secs(config.http_client_connect_timeout_secs))
.read_timeout(Duration::from_secs(config.http_client_read_timeout_secs))
.pool_max_idle_per_host(config.http_client_pool_size)
.pool_idle_timeout(Duration::from_secs(
config.http_client_pool_idle_timeout_secs,
))
.max_retries(config.http_client_max_retries)
.retry_initial_delay(Duration::from_millis(
config.http_client_retry_initial_delay_ms,
))
.retry_max_delay(Duration::from_millis(config.http_client_retry_max_delay_ms))
}
pub struct RateLimiter {
semaphore: Arc<Semaphore>,
max_permits: usize,
}
impl RateLimiter {
#[must_use]
pub fn new(max_permits: usize) -> Self {
Self {
semaphore: Arc::new(Semaphore::new(max_permits)),
max_permits,
}
}
pub async fn acquire(&self) -> Result<tokio::sync::SemaphorePermit<'_>> {
self.semaphore
.acquire()
.await
.map_err(|e| Error::Other(format!("Failed to acquire rate limit permit: {e}")))
}
#[must_use]
pub fn try_acquire(&self) -> Option<tokio::sync::SemaphorePermit<'_>> {
self.semaphore.try_acquire().ok()
}
#[must_use]
pub fn available_permits(&self) -> usize {
self.semaphore.available_permits()
}
#[must_use]
pub fn max_permits(&self) -> usize {
self.max_permits
}
}
pub mod compression {
use crate::error::{Error, Result};
use flate2::write::GzEncoder;
use flate2::Compression;
use std::io::Write;
pub fn gzip_compress(data: &[u8]) -> Result<Vec<u8>> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder
.write_all(data)
.map_err(|e| Error::Other(format!("Gzip compression failed: {e}")))?;
encoder
.finish()
.map_err(|e| Error::Other(format!("Gzip compression finalize failed: {e}")))
}
pub fn gzip_decompress(data: &[u8]) -> Result<Vec<u8>> {
let mut decoder = flate2::read::GzDecoder::new(data);
let mut decompressed = Vec::new();
std::io::Read::read_to_end(&mut decoder, &mut decompressed)
.map_err(|e| Error::Other(format!("Gzip decompression failed: {e}")))?;
Ok(decompressed)
}
}
pub mod string {
#[must_use]
pub fn truncate_with_ellipsis(s: &str, max_len: usize) -> String {
if max_len <= 3 {
return "...".to_string();
}
let chars: Vec<char> = s.chars().collect();
if chars.len() <= max_len {
return s.to_string();
}
let truncated: String = chars.iter().take(max_len - 3).collect();
format!("{truncated}...")
}
pub fn parse_number<T: std::str::FromStr>(s: &str, default: T) -> T {
s.parse().unwrap_or(default)
}
#[must_use]
pub fn is_blank(s: &str) -> bool {
s.trim().is_empty()
}
}
pub mod time {
use chrono::{DateTime, Utc};
#[must_use]
pub fn current_timestamp_ms() -> i64 {
Utc::now().timestamp_millis()
}
#[must_use]
pub fn format_datetime(dt: &DateTime<Utc>) -> String {
dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string()
}
#[must_use]
pub fn elapsed_ms(start: std::time::Instant) -> u128 {
start.elapsed().as_millis()
}
}
pub mod validation {
use crate::error::Error;
const MAX_CRATE_NAME_LENGTH: usize = 100;
const MAX_VERSION_LENGTH: usize = 50;
const MAX_SEARCH_QUERY_LENGTH: usize = 200;
pub fn validate_crate_name(name: &str) -> Result<(), Error> {
if name.is_empty() {
return Err(Error::Other("Crate name cannot be empty".to_string()));
}
if name.len() > MAX_CRATE_NAME_LENGTH {
return Err(Error::Other("Crate name is too long".to_string()));
}
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Err(Error::Other(
"Crate name contains invalid characters".to_string(),
));
}
Ok(())
}
pub fn validate_version(version: &str) -> Result<(), Error> {
if version.is_empty() {
return Err(Error::Other("Version cannot be empty".to_string()));
}
if version.len() > MAX_VERSION_LENGTH {
return Err(Error::Other("Version is too long".to_string()));
}
if !version.chars().any(|c| c.is_ascii_digit()) {
return Err(Error::Other("Version must contain digits".to_string()));
}
Ok(())
}
pub fn validate_search_query(query: &str) -> Result<(), Error> {
if query.is_empty() {
return Err(Error::Other("Search query cannot be empty".to_string()));
}
if query.len() > MAX_SEARCH_QUERY_LENGTH {
return Err(Error::Other("Search query is too long".to_string()));
}
Ok(())
}
}
pub mod metrics {
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Instant;
#[derive(Clone)]
pub struct PerformanceCounter {
total_requests: Arc<AtomicU64>,
successful_requests: Arc<AtomicU64>,
failed_requests: Arc<AtomicU64>,
total_response_time_ms: Arc<AtomicU64>,
}
impl PerformanceCounter {
#[must_use]
pub fn new() -> Self {
Self {
total_requests: Arc::new(AtomicU64::new(0)),
successful_requests: Arc::new(AtomicU64::new(0)),
failed_requests: Arc::new(AtomicU64::new(0)),
total_response_time_ms: Arc::new(AtomicU64::new(0)),
}
}
#[must_use]
pub fn record_request_start(&self) -> Instant {
self.total_requests.fetch_add(1, Ordering::Relaxed);
Instant::now()
}
#[allow(clippy::cast_possible_truncation)]
pub fn record_request_complete(&self, start: Instant, success: bool) {
let duration_ms = start.elapsed().as_millis() as u64;
self.total_response_time_ms
.fetch_add(duration_ms, Ordering::Relaxed);
if success {
self.successful_requests.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_requests.fetch_add(1, Ordering::Relaxed);
}
}
#[must_use]
pub fn get_stats(&self) -> PerformanceStats {
let total = self.total_requests.load(Ordering::Relaxed);
let success = self.successful_requests.load(Ordering::Relaxed);
let failed = self.failed_requests.load(Ordering::Relaxed);
let total_time = self.total_response_time_ms.load(Ordering::Relaxed);
#[allow(clippy::cast_precision_loss)]
let avg_response_time = if total > 0 {
total_time as f64 / total as f64
} else {
0.0
};
#[allow(clippy::cast_precision_loss)]
let success_rate = if total > 0 {
success as f64 / total as f64 * 100.0
} else {
0.0
};
PerformanceStats {
total_requests: total,
successful_requests: success,
failed_requests: failed,
average_response_time_ms: avg_response_time,
success_rate_percent: success_rate,
}
}
pub fn reset(&self) {
self.total_requests.store(0, Ordering::Relaxed);
self.successful_requests.store(0, Ordering::Relaxed);
self.failed_requests.store(0, Ordering::Relaxed);
self.total_response_time_ms.store(0, Ordering::Relaxed);
}
}
impl Default for PerformanceCounter {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct PerformanceStats {
pub total_requests: u64,
pub successful_requests: u64,
pub failed_requests: u64,
pub average_response_time_ms: f64,
pub success_rate_percent: f64,
}
}