Skip to main content

obj_cli/
lib.rs

1//! `obj-cli` — the `obj` command-line binary's library half.
2//!
3//! [`run`] is the entry point: it parses arguments via clap's
4//! derive API, dispatches into the per-subcommand handler, and
5//! returns the process exit code the binary should propagate.
6//!
7//! Exit codes follow classic Unix conventions:
8//!
9//! | code | meaning                                                |
10//! |------|--------------------------------------------------------|
11//! | 0    | success (incl. explicitly-requested `--help`/`--version`)|
12//! | 1    | "the answer is bad" — content-level failure (corruption)|
13//! | 2    | "couldn't even ask" — I/O / missing path / engine error|
14//! | 3    | argument-parse failure                                  |
15//!
16//! Putting the logic in a library half keeps the binary's `main`
17//! a thin shim — and makes integration testing via `assert_cmd`
18//! straightforward without exec-vs-fn skew.
19
20#![forbid(unsafe_code)]
21#![deny(missing_docs)]
22#![deny(rustdoc::broken_intra_doc_links)]
23
24use std::path::PathBuf;
25
26use clap::error::ErrorKind;
27use clap::{Parser, Subcommand, ValueEnum};
28
29mod backup;
30mod check;
31mod dump;
32mod stat;
33
34/// Top-level CLI definition. The binary entry point is [`run`],
35/// which dispatches into the per-subcommand handlers in private
36/// submodules below.
37#[derive(Debug, Parser)]
38#[command(
39    name = "obj",
40    about = "Command-line tools for `obj` — embedded document database.",
41    version,
42    propagate_version = true
43)]
44struct Cli {
45    #[command(subcommand)]
46    command: Command,
47}
48
49/// The set of subcommands the CLI accepts.
50///
51/// M12 lands the full quartet: `check`, `stat`, `dump`, `backup`.
52#[derive(Debug, Subcommand)]
53enum Command {
54    /// Run the full bidirectional integrity check on a database
55    /// file and report success / failure.
56    Check {
57        /// Path to the `.obj` file to check.
58        path: PathBuf,
59    },
60    /// Print a structured summary of the file: header info, page
61    /// counts, and per-collection statistics.
62    Stat {
63        /// Path to the `.obj` file to inspect.
64        path: PathBuf,
65    },
66    /// Walk a collection's primary tree and print one record per
67    /// step. The default `--format header` prints the per-doc
68    /// header struct; `--format hex` adds the raw payload bytes
69    /// in hex. No schema-aware decode is performed (the CLI does
70    /// not know about user-defined `Document` types).
71    Dump {
72        /// Path to the `.obj` file to inspect.
73        path: PathBuf,
74        /// Name of the collection to walk.
75        #[arg(long)]
76        collection: String,
77        /// Maximum number of documents to print. `0` = unbounded.
78        #[arg(long, default_value_t = 20)]
79        limit: usize,
80        /// Output format. `header` (default) prints the per-doc
81        /// header struct; `hex` adds the payload bytes in hex.
82        #[arg(long, value_enum, default_value_t = DumpFormat::Header)]
83        format: DumpFormat,
84    },
85    /// Take a hot backup of `src` to `dest`. Writers against
86    /// `src` continue uninterrupted. The destination file MUST
87    /// NOT already exist.
88    Backup {
89        /// Path to the source `.obj` file.
90        src: PathBuf,
91        /// Path the backup will be written to.
92        dest: PathBuf,
93    },
94}
95
96/// Output format for `obj dump`.
97#[derive(Debug, Clone, Copy, ValueEnum)]
98pub(crate) enum DumpFormat {
99    /// Per-doc header only — collection id, type version, payload
100    /// length, payload CRC32C — plus the primary id.
101    Header,
102    /// Same as `header`, with the payload bytes appended as hex.
103    Hex,
104}
105
106/// Parse `std::env::args()` and dispatch into the matched
107/// subcommand. Returns the process exit code the binary should
108/// propagate to the OS.
109///
110/// On argument-parse failure clap exits the process with code 2 by
111/// default; we override [`clap::Error::exit_code`] indirectly via
112/// the [`Cli::try_parse`] path so the binary's `main` can translate
113/// it into the canonical "3 = argument error" exit-code documented
114/// at the top of this module.
115///
116/// Power-of-ten Rule 4 — `run` itself is short; each subcommand
117/// handler lives in its own helper. Rule 7 — every fallible call
118/// is matched or `?`-propagated; no `unwrap` on the path.
119#[must_use]
120pub fn run() -> i32 {
121    let cli = match Cli::try_parse() {
122        Ok(c) => c,
123        Err(err) => {
124            // Surface clap's formatted output then collapse its
125            // varied internal exit-code conventions onto the
126            // CLI-level codes. `--help` / `--version` are an
127            // explicitly-requested *success* per standard CLI
128            // convention (exit 0), so map those display kinds to 0
129            // and reserve "3 = arg failure" for genuine parse
130            // errors.
131            // `err.print()` only fails if writing to the relevant
132            // stream fails; ignore that failure path (`Result` is
133            // documented at the trait — power-of-ten Rule 7).
134            let _ = err.print();
135            return match err.kind() {
136                ErrorKind::DisplayHelp
137                | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
138                | ErrorKind::DisplayVersion => 0,
139                _ => 3,
140            };
141        }
142    };
143    match cli.command {
144        Command::Check { path } => check::run(&path),
145        Command::Stat { path } => stat::run(&path),
146        Command::Dump {
147            path,
148            collection,
149            limit,
150            format,
151        } => dump::run(&path, &collection, limit, format),
152        Command::Backup { src, dest } => backup::run(&src, &dest),
153    }
154}