obj-cli 1.0.0

Command-line tools (dump, check, stat, backup) for the obj embedded document database.
Documentation
//! `obj-cli` — the `obj` command-line binary's library half.
//!
//! [`run`] is the entry point: it parses arguments via clap's
//! derive API, dispatches into the per-subcommand handler, and
//! returns the process exit code the binary should propagate.
//!
//! Exit codes follow classic Unix conventions:
//!
//! | code | meaning                                                |
//! |------|--------------------------------------------------------|
//! | 0    | success                                                |
//! | 1    | "the answer is bad" — content-level failure (corruption)|
//! | 2    | "couldn't even ask" — I/O / missing path / engine error|
//! | 3    | argument-parse failure (clap default)                  |
//!
//! Putting the logic in a library half keeps the binary's `main`
//! a thin shim — and makes integration testing via `assert_cmd`
//! straightforward without exec-vs-fn skew.

#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]

use std::path::PathBuf;

use clap::{Parser, Subcommand, ValueEnum};

mod backup;
mod check;
mod dump;
mod stat;

/// Top-level CLI definition. The binary entry point is [`run`],
/// which dispatches into the per-subcommand handlers in private
/// submodules below.
#[derive(Debug, Parser)]
#[command(
    name = "obj",
    about = "Command-line tools for `obj` — embedded document database.",
    version,
    propagate_version = true
)]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

/// The set of subcommands the CLI accepts.
///
/// M12 lands the full quartet: `check`, `stat`, `dump`, `backup`.
#[derive(Debug, Subcommand)]
enum Command {
    /// Run the full bidirectional integrity check on a database
    /// file and report success / failure.
    Check {
        /// Path to the `.obj` file to check.
        path: PathBuf,
    },
    /// Print a structured summary of the file: header info, page
    /// counts, and per-collection statistics.
    Stat {
        /// Path to the `.obj` file to inspect.
        path: PathBuf,
    },
    /// Walk a collection's primary tree and print one record per
    /// step. The default `--format header` prints the per-doc
    /// header struct; `--format hex` adds the raw payload bytes
    /// in hex. No schema-aware decode is performed (the CLI does
    /// not know about user-defined `Document` types).
    Dump {
        /// Path to the `.obj` file to inspect.
        path: PathBuf,
        /// Name of the collection to walk.
        #[arg(long)]
        collection: String,
        /// Maximum number of documents to print. `0` = unbounded.
        #[arg(long, default_value_t = 20)]
        limit: usize,
        /// Output format. `header` (default) prints the per-doc
        /// header struct; `hex` adds the payload bytes in hex.
        #[arg(long, value_enum, default_value_t = DumpFormat::Header)]
        format: DumpFormat,
    },
    /// Take a hot backup of `src` to `dest`. Writers against
    /// `src` continue uninterrupted. The destination file MUST
    /// NOT already exist.
    Backup {
        /// Path to the source `.obj` file.
        src: PathBuf,
        /// Path the backup will be written to.
        dest: PathBuf,
    },
}

/// Output format for `obj dump`.
#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum DumpFormat {
    /// Per-doc header only — collection id, type version, payload
    /// length, payload CRC32C — plus the primary id.
    Header,
    /// Same as `header`, with the payload bytes appended as hex.
    Hex,
}

/// Parse `std::env::args()` and dispatch into the matched
/// subcommand. Returns the process exit code the binary should
/// propagate to the OS.
///
/// On argument-parse failure clap exits the process with code 2 by
/// default; we override [`clap::Error::exit_code`] indirectly via
/// the [`Cli::try_parse`] path so the binary's `main` can translate
/// it into the canonical "3 = argument error" exit-code documented
/// at the top of this module.
///
/// Power-of-ten Rule 4 — `run` itself is short; each subcommand
/// handler lives in its own helper. Rule 7 — every fallible call
/// is matched or `?`-propagated; no `unwrap` on the path.
#[must_use]
pub fn run() -> i32 {
    let cli = match Cli::try_parse() {
        Ok(c) => c,
        Err(err) => {
            // Surface clap's formatted error then collapse its
            // varied internal exit-code conventions onto the
            // CLI-level "3 = arg failure" code.
            // `err.print()` only fails if writing to stderr fails;
            // ignore that failure path (`Result` is documented at
            // the trait — power-of-ten Rule 7).
            let _ = err.print();
            return 3;
        }
    };
    match cli.command {
        Command::Check { path } => check::run(&path),
        Command::Stat { path } => stat::run(&path),
        Command::Dump {
            path,
            collection,
            limit,
            format,
        } => dump::run(&path, &collection, limit, format),
        Command::Backup { src, dest } => backup::run(&src, &dest),
    }
}