webpage_quality_analyzer 1.0.2

High-performance webpage quality analyzer with 115 comprehensive metrics - Rust library with WASM, C++, and Python bindings
Documentation
//! Tokio-based async runtime for native/server environments
//!
//! This implementation uses:
//! - tokio for async runtime
//! - reqwest for HTTP client with connection pooling
//! - Parallel execution via tokio::spawn

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;

/// Global HTTP client with connection pooling
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")
});

/// Tokio-based runtime implementation
#[derive(Debug, Clone, Default)]
pub struct TokioRuntime;

impl TokioRuntime {
    pub fn new() -> Self {
        Self
    }

    /// Convert reqwest response to FetchResult
    async fn response_to_fetch_result(
        url: String,
        response: reqwest::Response,
    ) -> Result<FetchResult, AnalyzeError> {
        let status = response.status().as_u16();

        // Extract headers
        let headers: Vec<(String, String)> = response
            .headers()
            .iter()
            .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
            .collect();

        // Get body
        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();

        // Test with a reliable URL (Google)
        let result = runtime.check_url("https://www.google.com").await;
        assert!(result.is_ok());
        assert!(result.unwrap());
    }
}