use super::{AsyncRuntime, FetchResult};
use crate::AnalyzeError;
use once_cell::sync::Lazy;
use reqwest::{Client, StatusCode};
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
static HTTP_CLIENT: Lazy<Client> = Lazy::new(|| {
Client::builder()
.pool_max_idle_per_host(10)
.pool_idle_timeout(Duration::from_secs(90))
.timeout(Duration::from_secs(30))
.user_agent("WebpageQualityAnalyzer/0.1.0")
.build()
.expect("Failed to build HTTP client")
});
#[derive(Debug, Clone, Default)]
pub struct TokioRuntime;
impl TokioRuntime {
pub fn new() -> Self {
Self
}
async fn response_to_fetch_result(
url: String,
response: reqwest::Response,
) -> Result<FetchResult, AnalyzeError> {
let status = response.status().as_u16();
let headers: Vec<(String, String)> = response
.headers()
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect();
let body = response.text().await.map_err(|e| {
AnalyzeError::NetworkError(format!("Failed to read response body: {}", e))
})?;
Ok(FetchResult {
url,
status,
body,
headers,
})
}
}
impl AsyncRuntime for TokioRuntime {
fn fetch_url(
&self,
url: &str,
) -> Pin<Box<dyn Future<Output = Result<FetchResult, AnalyzeError>> + Send + '_>> {
let url = url.to_string();
Box::pin(async move {
let response =
HTTP_CLIENT.get(&url).send().await.map_err(|e| {
AnalyzeError::NetworkError(format!("HTTP request failed: {}", e))
})?;
if !response.status().is_success() {
return Err(AnalyzeError::NetworkError(format!(
"HTTP error {}: {}",
response.status().as_u16(),
url
)));
}
Self::response_to_fetch_result(url.clone(), response).await
})
}
fn check_url(
&self,
url: &str,
) -> Pin<Box<dyn Future<Output = Result<bool, AnalyzeError>> + Send + '_>> {
let url = url.to_string();
Box::pin(async move {
match HTTP_CLIENT.head(&url).send().await {
Ok(response) => {
let status = response.status();
Ok(status.is_success()
|| status == StatusCode::MOVED_PERMANENTLY
|| status == StatusCode::FOUND)
}
Err(_) => Ok(false),
}
})
}
fn fetch_batch(
&self,
urls: Vec<String>,
) -> Pin<Box<dyn Future<Output = Vec<Result<FetchResult, AnalyzeError>>> + Send + '_>> {
Box::pin(async move {
let mut tasks = Vec::new();
for url in urls {
let url_clone = url.clone();
let task = tokio::spawn(async move {
let response = HTTP_CLIENT.get(&url_clone).send().await.map_err(|e| {
AnalyzeError::NetworkError(format!("HTTP request failed: {}", e))
})?;
if !response.status().is_success() {
return Err(AnalyzeError::NetworkError(format!(
"HTTP error {}: {}",
response.status().as_u16(),
url_clone
)));
}
TokioRuntime::response_to_fetch_result(url_clone.clone(), response).await
});
tasks.push(task);
}
let mut results = Vec::new();
for task in tasks {
match task.await {
Ok(result) => results.push(result),
Err(e) => results.push(Err(AnalyzeError::NetworkError(format!(
"Task join error: {}",
e
)))),
}
}
results
})
}
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
tokio::spawn(future);
}
fn runtime_name(&self) -> &'static str {
"tokio"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_tokio_runtime_name() {
let runtime = TokioRuntime::new();
assert_eq!(runtime.runtime_name(), "tokio");
}
#[tokio::test]
async fn test_check_url() {
let runtime = TokioRuntime::new();
let result = runtime.check_url("https://www.google.com").await;
assert!(result.is_ok());
assert!(result.unwrap());
}
}