patholog 1.0.0

Diagnose, explain, clean, and safely repair PATH behavior.
Documentation
use std::collections::BTreeMap;

use crate::model::{Diagnostic, DoctorReport, HealthReport, HealthSeverity, IssueKind};

pub(crate) fn summarize_health(report: DoctorReport) -> HealthReport {
    let entry_count = report.entries.len();
    let issue_count = report.diagnostics.len();
    let counts = count_diagnostics(&report.diagnostics);
    let worst_severity = report
        .diagnostics
        .iter()
        .map(|diagnostic| severity_for(diagnostic.kind))
        .max()
        .unwrap_or(HealthSeverity::None);
    let penalty = report.diagnostics.iter().fold(0u8, |penalty, diagnostic| {
        penalty.saturating_add(penalty_for(diagnostic.kind))
    });
    let score = 100u8.saturating_sub(penalty);
    let healthy = score == 100 && issue_count == 0;

    HealthReport {
        variable: report.variable,
        score,
        healthy,
        entry_count,
        issue_count,
        worst_severity,
        counts,
        diagnostics: report.diagnostics,
    }
}

fn count_diagnostics(diagnostics: &[Diagnostic]) -> BTreeMap<IssueKind, usize> {
    let mut counts = BTreeMap::new();
    for diagnostic in diagnostics {
        *counts.entry(diagnostic.kind).or_default() += 1;
    }
    counts
}

fn severity_for(kind: IssueKind) -> HealthSeverity {
    match kind {
        IssueKind::Empty | IssueKind::Missing | IssueKind::NotDirectory | IssueKind::Unreadable => {
            HealthSeverity::Error
        }
        IssueKind::Duplicate
        | IssueKind::SuspiciousOrder
        | IssueKind::ShadowedCommand
        | IssueKind::Unwanted => HealthSeverity::Warning,
    }
}

fn penalty_for(kind: IssueKind) -> u8 {
    match severity_for(kind) {
        HealthSeverity::Error => 15,
        HealthSeverity::Warning => 5,
        HealthSeverity::None => 0,
    }
}

#[cfg(test)]
#[path = "health/tests.rs"]
mod tests;