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}