upstream-rs 2.5.0

Fetch package updates directly from the source.
Documentation
use anyhow::{Result, anyhow};
use console::style;
use serde::Serialize;

use crate::{
    output::{self, Status},
    routines::doctor::{self, DoctorReport, Level},
};

fn status_for_level(level: Level) -> Status {
    match level {
        Level::Ok => Status::Ok,
        Level::Warn => Status::Warn,
        Level::Fail => Status::Fail,
    }
}

fn print_verbose_findings(report: &DoctorReport) {
    for finding in &report.findings {
        println!(
            "{} {}",
            output::status_label(status_for_level(finding.level)),
            finding.message
        );
    }
}

fn print_summary(report: &DoctorReport) {
    println!("{}/{} checks ok", report.ok, report.total_checks());
    if !report.warnings.is_empty() {
        println!();
        println!("{}", style("warnings:").yellow());
        for warning in &report.warnings {
            println!(" - {}", warning);
        }
    }

    if !report.failures.is_empty() {
        println!();
        println!("{}", style("failures:").red());
        for failure in &report.failures {
            println!(" - {}", failure);
        }
    }
}

fn print_hints(report: &DoctorReport) {
    if report.hints.is_empty() {
        return;
    }

    println!();
    println!("{}", style("Suggested fixes:").cyan());
    for hint in &report.hints {
        println!(" - {}", hint);
    }
}

pub async fn run(names: Vec<String>, verbose: bool, fix: bool, json: bool) -> Result<()> {
    if json {
        let report = doctor::run(names, fix).await?;
        println!(
            "{}",
            serde_json::to_string_pretty(&json_doctor_report(&report))?
        );
        if report.fail > 0 {
            return Err(anyhow!(
                "Doctor found {} failure(s). Resolve reported issues and retry.",
                report.fail
            ));
        }
        return Ok(());
    }

    println!("{}", style("Running upstream doctor...").cyan());

    let report = doctor::run(names, fix).await?;
    if verbose {
        print_verbose_findings(&report);
    }

    println!();
    print_summary(&report);
    print_hints(&report);

    if report.fail > 0 {
        return Err(anyhow!(
            "Doctor found {} failure(s). Resolve reported issues and retry.",
            report.fail
        ));
    }

    if report.warn > 0 {
        println!(
            "{}",
            style("Doctor completed with warnings. Review the items above.").yellow()
        );
    } else {
        println!("{}", style("Doctor completed successfully.").green());
    }

    Ok(())
}

#[derive(Serialize)]
struct JsonDoctorReport {
    status: &'static str,
    ok: u32,
    warn: u32,
    fail: u32,
    total: u32,
    findings: Vec<JsonDoctorFinding>,
    warnings: Vec<String>,
    failures: Vec<String>,
    hints: Vec<String>,
}

#[derive(Serialize)]
struct JsonDoctorFinding {
    level: &'static str,
    message: String,
}

fn json_doctor_report(report: &DoctorReport) -> JsonDoctorReport {
    JsonDoctorReport {
        status: if report.fail > 0 {
            "failed"
        } else if report.warn > 0 {
            "warning"
        } else {
            "ok"
        },
        ok: report.ok,
        warn: report.warn,
        fail: report.fail,
        total: report.total_checks(),
        findings: report
            .findings
            .iter()
            .map(|finding| JsonDoctorFinding {
                level: level_label(finding.level),
                message: finding.message.clone(),
            })
            .collect(),
        warnings: report.warnings.clone(),
        failures: report.failures.clone(),
        hints: report.hints.clone(),
    }
}

fn level_label(level: Level) -> &'static str {
    match level {
        Level::Ok => "ok",
        Level::Warn => "warn",
        Level::Fail => "fail",
    }
}

#[cfg(test)]
mod tests {
    use super::json_doctor_report;
    use crate::routines::doctor::{DoctorFinding, DoctorReport, Level};

    #[test]
    fn json_doctor_report_serializes_summary_and_findings() {
        let report = DoctorReport {
            ok: 2,
            warn: 1,
            fail: 1,
            findings: vec![
                DoctorFinding {
                    level: Level::Ok,
                    message: "config exists".to_string(),
                },
                DoctorFinding {
                    level: Level::Fail,
                    message: "symlink missing".to_string(),
                },
            ],
            warnings: vec!["PATH file missing".to_string()],
            failures: vec!["symlink missing".to_string()],
            hints: vec!["Run `upstream hooks init`.".to_string()],
        };

        let json =
            serde_json::to_value(json_doctor_report(&report)).expect("serialize doctor report");

        assert_eq!(json["status"], "failed");
        assert_eq!(json["total"], 4);
        assert_eq!(json["findings"][0]["level"], "ok");
        assert_eq!(json["findings"][1]["level"], "fail");
        assert_eq!(json["hints"][0], "Run `upstream hooks init`.");
    }
}