geopackage-cli 0.11.0

Command-line tool for OGC GeoPackage (.gpkg) files: inspect, validate, index, repair and copy, and read tiles and coverages
//! `gpkg validate`: what this crate can see wrong with a file.
//!
//! The command opens files read-only and in lenient mode, for the same reason
//! as `gpkg info`. A file with nothing wrong with it does not need validating,
//! so refusing to open the problem files would defeat the purpose of the
//! command.
//!
//! The command modifies nothing. Where a finding has a repair, the advice names
//! the library method that performs it, and you decide whether to run it.

use std::io::Write;
use std::path::Path;
use std::process::ExitCode;

use geopackage::{GeoPackage, Severity};

use crate::error::Result;

pub fn run(path: &Path, strict: bool, out: &mut impl Write) -> Result<ExitCode> {
    let gpkg = GeoPackage::open_read_only_lenient(path)?;
    let findings = gpkg.validate()?;

    writeln!(out, "{}", path.display())?;
    if findings.is_empty() {
        // Said explicitly, because a silent exit reads as "did not run". The
        // qualification matters: this is what we can see, not conformance.
        writeln!(
            out,
            "  no findings (this crate's checks; the OGC ETS is the authority)"
        )?;
        return Ok(ExitCode::SUCCESS);
    }

    // `validate` returns findings most severe first, so they print in the order
    // a reader should care about them.
    for finding in &findings {
        writeln!(out, "  {}: {finding}", finding.severity())?;
        if let Some(repair) = finding.repair() {
            writeln!(out, "    repair: {repair}")?;
        }
    }

    let errors = count(&findings, Severity::Error);
    let warnings = count(&findings, Severity::Warning);
    let advisories = count(&findings, Severity::Advisory);
    writeln!(
        out,
        "\n  {}, {}, {}",
        plural(errors, "error", "errors"),
        plural(warnings, "warning", "warnings"),
        plural(advisories, "advisory", "advisories")
    )?;

    // An error means a reader can get a wrong answer, which is worth a failing
    // exit status so a script or a CI job notices. Warnings and advisories are
    // not, unless the caller asks for `--strict`.
    let failed = if strict {
        errors + warnings > 0
    } else {
        errors > 0
    };
    Ok(if failed {
        ExitCode::FAILURE
    } else {
        ExitCode::SUCCESS
    })
}

fn count(findings: &[geopackage::Finding], severity: Severity) -> usize {
    findings
        .iter()
        .filter(|finding| finding.severity() == severity)
        .count()
}

/// Formats a count with its noun, as in `1 error` or `0 errors`. The caller
/// passes the plural rather than deriving it, because "advisory" does not
/// pluralise by adding an `s`.
fn plural(count: usize, one: &str, many: &str) -> String {
    if count == 1 {
        format!("{count} {one}")
    } else {
        format!("{count} {many}")
    }
}