webpage_quality_analyzer 1.0.2

High-performance webpage quality analyzer with 115 comprehensive metrics - Rust library with WASM, C++, and Python bindings
Documentation
//! Link checking functionality for validati#[cfg(feature = "linkcheck")]
/// Result of checking a single link with structured error information
#[derive(Debug, Clone)]
pub struct LinkCheckResult {
    pub url: String,
    pub status_code: Option<u16>,
    pub is_broken: bool,
    pub error_type: Option<LinkCheckErrorType>,
    pub error_message: Option<String>,
    pub response_time_ms: u64,
}

#[cfg(feature = "linkcheck")]
/// Structured error types for link checking
#[derive(Debug, Clone)]
pub enum LinkCheckErrorType {
    HttpError(u16),
    NetworkError,
    Timeout,
    InvalidUrl,
    TlsError,
    ConnectionError,
}

#[cfg(feature = "linkcheck")]
use crate::models::models::LinkInfo;
/// This module provides asynchronous link checking capabilities to detect
/// broken links and calculate broken link rates for quality analysis.
use crate::models::models::{AnalyzeError, Result};
#[cfg(feature = "linkcheck")]
use reqwest::Client;
#[cfg(feature = "linkcheck")]
use std::time::Duration;
#[cfg(feature = "linkcheck")]
use tokio::time::timeout;

#[cfg(feature = "linkcheck")]
/// Link checking configuration
#[derive(Debug, Clone)]
pub struct LinkCheckConfig {
    /// Maximum number of links to check
    pub max_links: usize,
    /// Request timeout in seconds
    pub timeout_seconds: u64,
    /// Maximum concurrent requests
    pub max_concurrent: usize,
    /// User agent string for requests
    pub user_agent: String,
    /// Whether to follow redirects
    pub follow_redirects: bool,
}

#[cfg(feature = "linkcheck")]
impl Default for LinkCheckConfig {
    fn default() -> Self {
        Self {
            max_links: 30,
            timeout_seconds: 10,
            max_concurrent: 5,
            user_agent: "WebpageQualityAnalyzer/0.1.0".to_owned(),
            follow_redirects: true,
        }
    }
}

#[cfg(feature = "linkcheck")]
/// Asynchronous link checker
#[derive(Debug)]
pub struct LinkChecker {
    client: Client,
    config: LinkCheckConfig,
}

#[cfg(feature = "linkcheck")]
impl LinkChecker {
    /// Create a new link checker with default configuration
    pub fn new() -> Result<Self> {
        let config = LinkCheckConfig::default();
        Self::with_config(config)
    }

    /// Create a link checker with custom configuration
    pub fn with_config(config: LinkCheckConfig) -> Result<Self> {
        let client = Client::builder()
            .user_agent(&config.user_agent)
            .redirect(if config.follow_redirects {
                reqwest::redirect::Policy::limited(3)
            } else {
                reqwest::redirect::Policy::none()
            })
            .timeout(Duration::from_secs(config.timeout_seconds))
            .build()
            .map_err(|e| AnalyzeError::LinkCheckError(e))?;

        Ok(Self { client, config })
    }

    /// Check a single link
    /// Check a single link
    pub async fn check_link(&self, url: &str) -> LinkCheckResult {
        let start_time = std::time::Instant::now();

        let result = timeout(
            Duration::from_secs(self.config.timeout_seconds),
            self.client.head(url).send(),
        )
        .await;

        let response_time_ms = start_time.elapsed().as_millis() as u64;

        match result {
            Ok(Ok(response)) => {
                let status_code = response.status().as_u16();
                let is_broken =
                    !response.status().is_success() && !response.status().is_redirection();

                LinkCheckResult {
                    url: url.to_owned(),
                    status_code: Some(status_code),
                    is_broken,
                    error_type: if is_broken {
                        Some(LinkCheckErrorType::HttpError(status_code))
                    } else {
                        None
                    },
                    error_message: if is_broken {
                        Some(format!("HTTP {}", status_code))
                    } else {
                        None
                    },
                    response_time_ms,
                }
            }
            Ok(Err(e)) => {
                let (error_type, error_message) = categorize_reqwest_error(&e);
                LinkCheckResult {
                    url: url.to_owned(),
                    status_code: None,
                    is_broken: true,
                    error_type: Some(error_type),
                    error_message: Some(error_message),
                    response_time_ms,
                }
            }
            Err(_) => LinkCheckResult {
                url: url.to_owned(),
                status_code: None,
                is_broken: true,
                error_type: Some(LinkCheckErrorType::Timeout),
                error_message: Some("Request timeout".to_owned()),
                response_time_ms,
            },
        }
    }

    /// Check multiple links with concurrency control
    pub async fn check_links(&self, links: &[LinkInfo]) -> Result<Vec<LinkCheckResult>> {
        use futures::stream::{self, StreamExt};

        // Filter and sample links
        let external_links: Vec<_> = links
            .iter()
            .filter(|link| {
                !link.is_internal
                    && !link.raw_href.starts_with('#')
                    && !link.raw_href.starts_with("mailto:")
            })
            .take(self.config.max_links)
            .collect();

        if external_links.is_empty() {
            return Ok(Vec::new());
        }

        // Check links with controlled concurrency
        let results = stream::iter(external_links)
            .map(|link| async move { self.check_link(&link.resolved_href).await })
            .buffer_unordered(self.config.max_concurrent)
            .collect::<Vec<_>>()
            .await;

        Ok(results)
    }

    /// Calculate broken link rate from check results
    pub fn calculate_broken_rate(&self, results: &[LinkCheckResult]) -> f32 {
        if results.is_empty() {
            return 0.0;
        }

        let broken_count = results.iter().filter(|r| r.is_broken).count();
        broken_count as f32 / results.len() as f32
    }

    /// Check links and return broken link rate
    pub async fn check_and_rate(&self, links: &[LinkInfo]) -> Result<f32> {
        let results = self.check_links(links).await?;
        Ok(self.calculate_broken_rate(&results))
    }
}

#[cfg(feature = "linkcheck")]
/// Create a default link checker
pub fn create_link_checker() -> Result<LinkChecker> {
    LinkChecker::new()
}

#[cfg(feature = "linkcheck")]
/// Create a link checker with custom configuration
pub fn create_link_checker_with_config(config: LinkCheckConfig) -> Result<LinkChecker> {
    LinkChecker::with_config(config)
}

// Stub implementations when linkcheck feature is disabled
#[cfg(not(feature = "linkcheck"))]
pub struct LinkChecker;

#[cfg(not(feature = "linkcheck"))]
impl LinkChecker {
    pub fn new() -> Result<Self> {
        Err(AnalyzeError::InternalError(
            "Link checking feature not enabled".to_owned(),
        ))
    }
}

#[cfg(feature = "linkcheck")]
/// Categorize reqwest errors into structured error types
fn categorize_reqwest_error(error: &reqwest::Error) -> (LinkCheckErrorType, String) {
    if error.is_timeout() {
        (LinkCheckErrorType::Timeout, "Request timeout".to_owned())
    } else if error.is_connect() {
        (
            LinkCheckErrorType::ConnectionError,
            format!("Connection failed: {}", error),
        )
    } else if error.is_request() {
        if error.to_string().contains("invalid URL") {
            (
                LinkCheckErrorType::InvalidUrl,
                format!("Invalid URL: {}", error),
            )
        } else {
            (
                LinkCheckErrorType::NetworkError,
                format!("Request error: {}", error),
            )
        }
    } else {
        (
            LinkCheckErrorType::NetworkError,
            format!("Network error: {}", error),
        )
    }
}