patholog 1.0.0

Diagnose, explain, clean, and safely repair PATH behavior.
Documentation
use serde_json::{Value, json};

use crate::config::{ConfigPolicy, LoadedConfig};
use crate::model::{
    ApplyOutcome, ApplyPlan, Diagnostic, DoctorReport, HealthReport, PathEntry, PathMutation,
    ResolutionCandidate, ResolutionReport, ShellProfile, ShellProfileScanReport, WhyNotReport,
};

pub(crate) fn dumps_json(value: &Value) -> Result<String, serde_json::Error> {
    serde_json::to_string_pretty(value).map(|json| format!("{json}\n"))
}

pub(crate) fn entries_to_json(entries: &[PathEntry]) -> Value {
    Value::Array(entries.iter().map(entry_to_json).collect())
}

pub(crate) fn doctor_to_json(report: &DoctorReport) -> Value {
    json!({
        "variable": report.variable.as_str(),
        "entries": report.entries.iter().map(entry_to_json).collect::<Vec<_>>(),
        "diagnostics": report.diagnostics.iter().map(diagnostic_to_json).collect::<Vec<_>>(),
    })
}

pub(crate) fn health_to_json(report: &HealthReport) -> Value {
    json!({
        "variable": report.variable.as_str(),
        "score": report.score,
        "healthy": report.healthy,
        "entry_count": report.entry_count,
        "issue_count": report.issue_count,
        "worst_severity": report.worst_severity.as_str(),
        "counts": health_counts_to_json(report),
        "diagnostics": report.diagnostics.iter().map(diagnostic_to_json).collect::<Vec<_>>(),
    })
}

pub(crate) fn resolution_to_json(report: &ResolutionReport) -> Value {
    let winner = report
        .candidates
        .iter()
        .find(|candidate| candidate.wins)
        .map(candidate_to_json);

    json!({
        "command": report.command,
        "found": winner.is_some(),
        "winner": winner,
        "candidates": report.candidates.iter().map(candidate_to_json).collect::<Vec<_>>(),
        "searched_directories": report.searched_directories,
        "related_hints": report.related_hints.iter().map(|hint| {
            json!({
                "command": hint.command,
                "paths": hint.paths,
            })
        }).collect::<Vec<_>>(),
    })
}

pub(crate) fn why_not_to_json(report: &WhyNotReport) -> Value {
    json!({
        "command": report.command,
        "found": report.found(),
        "winner": report.winner().map(candidate_to_json),
        "candidates": report.candidates.iter().map(candidate_to_json).collect::<Vec<_>>(),
        "searched_directories": report.searched_directories,
        "related_hints": report.related_hints.iter().map(|hint| {
            json!({
                "command": hint.command,
                "paths": hint.paths,
            })
        }).collect::<Vec<_>>(),
        "path_diagnostics": report
            .path_diagnostics
            .iter()
            .map(diagnostic_to_json)
            .collect::<Vec<_>>(),
        "advice": report.advice,
    })
}

pub(crate) fn shell_profile_scan_to_json(report: &ShellProfileScanReport) -> Value {
    json!({
        "home": report.home,
        "profiles": report.profiles.iter().map(shell_profile_to_json).collect::<Vec<_>>(),
    })
}

pub(crate) fn apply_plan_to_json(plan: &ApplyPlan) -> Value {
    json!({
        "shell": plan.shell.as_str(),
        "profile_path": plan.profile_path,
        "action": plan.action.as_str(),
        "existing_block": plan.existing_block,
        "planned_block": plan.planned_block,
        "cleaned_path": plan.cleaned_path,
        "would_write": plan.would_write,
    })
}

pub(crate) fn apply_outcome_to_json(outcome: &ApplyOutcome) -> Value {
    let mut value = apply_plan_to_json(&outcome.plan);
    if let Some(object) = value.as_object_mut() {
        object.insert("wrote".to_owned(), json!(outcome.wrote));
        object.insert("backup_path".to_owned(), json!(outcome.backup_path));
        object.insert("backup_created".to_owned(), json!(outcome.backup_created));
    }
    value
}

pub(crate) fn config_to_json(config: &LoadedConfig) -> Value {
    json!({
        "config_path": config.path.display().to_string(),
        "version": config.config.version,
        "path": config_policy_to_json(&config.config.path),
        "manpath": config_policy_to_json(&config.config.manpath),
    })
}

fn config_policy_to_json(policy: &ConfigPolicy) -> Value {
    json!({
        "drop": policy.drop_entries,
        "preset": policy
            .presets
            .iter()
            .map(|preset| preset.as_str())
            .collect::<Vec<_>>(),
        "fail_on": policy
            .fail_on
            .iter()
            .map(|kind| kind.as_str())
            .collect::<Vec<_>>(),
    })
}

fn entry_to_json(entry: &PathEntry) -> Value {
    json!({
        "index": entry.index,
        "raw": entry.raw,
        "display": entry.display,
        "comparison_key": entry.comparison_key,
        "kind": entry_kind(entry),
        "exists": entry.exists,
        "is_dir": entry.is_dir,
        "is_empty": entry.is_empty,
    })
}

fn entry_kind(entry: &PathEntry) -> &'static str {
    if entry.is_empty {
        "empty"
    } else if !entry.exists {
        "missing"
    } else if !entry.is_dir {
        "not_directory"
    } else {
        "directory"
    }
}

fn diagnostic_to_json(diagnostic: &Diagnostic) -> Value {
    json!({
        "kind": diagnostic.kind.as_str(),
        "message": diagnostic.message,
        "entry_index": diagnostic.entry_index,
        "entry_value": diagnostic.entry_value,
        "related_indexes": diagnostic.related_indexes,
    })
}

fn health_counts_to_json(report: &HealthReport) -> Value {
    let mut counts = serde_json::Map::new();
    for (kind, count) in &report.counts {
        counts.insert(kind.as_str().to_owned(), json!(count));
    }
    Value::Object(counts)
}

fn candidate_to_json(candidate: &ResolutionCandidate) -> Value {
    json!({
        "entry_index": candidate.entry_index,
        "directory": candidate.directory,
        "path": candidate.path,
        "wins": candidate.wins,
    })
}

fn shell_profile_to_json(profile: &ShellProfile) -> Value {
    json!({
        "shell": profile.shell,
        "path": profile.path,
        "exists": profile.exists,
        "is_file": profile.is_file,
        "readable": profile.readable,
        "path_mutations": profile.path_mutations.iter().map(path_mutation_to_json).collect::<Vec<_>>(),
    })
}

fn path_mutation_to_json(mutation: &PathMutation) -> Value {
    json!({
        "line": mutation.line,
        "kind": mutation.kind,
        "text": mutation.text,
    })
}