ironic 0.4.3

A batteries-included, type-safe application framework for Rust
Documentation
use std::{env, io::Write, path::Path, process::Command};

use crate::CliError;

pub(crate) fn execute(output: &mut impl Write) -> Result<(), CliError> {
    check_tool("Rust", "rustc", &["--version"], output)?;
    check_tool("Cargo", "cargo", &["--version"], output)?;

    let root =
        env::current_dir().map_err(|error| CliError::io("read current directory", ".", error))?;
    let manifest = root.join("Cargo.toml");
    report(
        output,
        "Project manifest",
        manifest.is_file(),
        &manifest.display().to_string(),
    )?;
    let framework = manifest_contains(&manifest, "ironic")?;
    report(
        output,
        "Ironic dependency",
        framework,
        if framework { "found" } else { "not found" },
    )?;

    check_ironic_version(output)?;

    Ok(())
}

fn check_ironic_version(output: &mut impl Write) -> Result<(), CliError> {
    let current = env!("CARGO_PKG_VERSION");
    let latest = match super::update::check_latest_version() {
        Ok(Some(version)) => version,
        Ok(None) => {
            return report(output, "CLI version", true, current);
        }
        Err(error) => {
            return report(
                output,
                "CLI version",
                true,
                &format!("{current} (could not check for updates: {error})"),
            );
        }
    };
    if latest == current {
        report(output, "CLI version", true, &format!("{current} (latest)"))
    } else {
        report(
            output,
            "CLI version",
            false,
            &format!("{current} (latest: {latest}; run `ironic update`)"),
        )
    }
}

fn check_tool(
    label: &str,
    program: &str,
    arguments: &[&str],
    output: &mut impl Write,
) -> Result<(), CliError> {
    match Command::new(program).args(arguments).output() {
        Ok(result) if result.status.success() => {
            let detail = String::from_utf8_lossy(&result.stdout);
            report(output, label, true, detail.trim())
        }
        Ok(result) => report(output, label, false, &result.status.to_string()),
        Err(error) => report(output, label, false, &error.to_string()),
    }
}

fn manifest_contains(path: &Path, needle: &str) -> Result<bool, CliError> {
    if !path.is_file() {
        return Ok(false);
    }
    let source =
        std::fs::read_to_string(path).map_err(|error| CliError::io("read", path, error))?;
    Ok(source.contains(needle))
}

fn report(
    output: &mut impl Write,
    label: &str,
    success: bool,
    detail: &str,
) -> Result<(), CliError> {
    writeln!(
        output,
        "{label:<22} {} {detail}",
        if success { "OK" } else { "WARN" }
    )
    .map_err(|error| CliError::io("write output", "stdout", error))
}