obj-cli 1.0.0

Command-line tools (dump, check, stat, backup) for the obj embedded document database.
Documentation
//! `obj check <path>` — full bidirectional integrity walk.
//!
//! Calls [`obj::Db::open`] (which runs the M11 #91 lightweight
//! open-time check) and then [`obj::Db::integrity_check`] (the
//! M11 #90 full walk). The exit-code matrix:
//!
//! | outcome                                     | code |
//! |---------------------------------------------|------|
//! | `report.is_ok()`                            | 0    |
//! | content-level failures (corruption)         | 1    |
//! | I/O / open / pager / engine error           | 2    |
//!
//! The trio mirrors classic Unix conventions: 0 = ok, 1 = answer
//! is bad, 2 = couldn't even ask.

use std::path::Path;

use obj::{Db, IntegrityFailure, IntegrityReport};

/// Run `obj check <path>` and return the process exit code.
///
/// Power-of-ten Rule 4: the body stays under 60 lines by deferring
/// formatting to the helpers below.
pub(crate) fn run(path: &Path) -> i32 {
    let db = match Db::open(path) {
        Ok(db) => db,
        Err(err) => {
            print_open_failure(path, &err);
            return open_exit_code(&err);
        }
    };
    let report = match db.integrity_check() {
        Ok(r) => r,
        Err(err) => {
            eprintln!("error: integrity_check failed: {err}");
            return 2;
        }
    };
    if report.is_ok() {
        print_ok(&report);
        0
    } else {
        print_failures(&report);
        1
    }
}

/// Print the one-line success summary. Format:
/// `ok: <pages_checked> pages, <elapsed-millis> ms`.
fn print_ok(report: &IntegrityReport) {
    let millis = report.elapsed.as_millis();
    println!("ok: {} pages, {millis} ms", report.pages_checked);
}

/// Print every failure on its own line. Each failure is rendered
/// via its `Debug` formatter — stable enough for grepping in CI
/// without committing to a stable `Display` impl.
fn print_failures(report: &IntegrityReport) {
    let millis = report.elapsed.as_millis();
    eprintln!(
        "failed: {} pages checked, {millis} ms, {} failure(s):",
        report.pages_checked,
        report.failures.len(),
    );
    for failure in &report.failures {
        eprintln!("  - {}", format_failure(failure));
    }
}

/// Compact one-line representation of an `IntegrityFailure`.
/// `Debug` keeps the variant + struct-field shape stable across
/// formatter changes; an explicit `Display` impl could replace
/// this in a later milestone if a stable format becomes part of
/// the CLI's public contract.
fn format_failure(failure: &IntegrityFailure) -> String {
    format!("{failure:?}")
}

/// Render the open-time error to stderr with a short hint. The
/// caller decides the exit-code; this helper is print-only so the
/// two concerns stay separable (power-of-ten Rule 4).
fn print_open_failure(path: &Path, err: &obj::Error) {
    eprintln!("error: failed to open {}: {err}", path.display());
}

/// Map an open-time error to the canonical CLI exit code.
///
/// `Error::Corruption` and `Error::BTreeDepthExceeded` are
/// content-level failures (the M11 #91 fast check surfacing as a
/// hard open error) → exit code 1. Everything else — `Error::Io`,
/// missing path, permission denied, etc. — is "couldn't even ask"
/// → exit code 2.
fn open_exit_code(err: &obj::Error) -> i32 {
    match err {
        obj::Error::Corruption { .. } | obj::Error::BTreeDepthExceeded { .. } => 1,
        _ => 2,
    }
}