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 |
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 (clap default) |
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::{Parser, Subcommand, ValueEnum};
27
28mod backup;
29mod check;
30mod dump;
31mod stat;
32
33/// Top-level CLI definition. The binary entry point is [`run`],
34/// which dispatches into the per-subcommand handlers in private
35/// submodules below.
36#[derive(Debug, Parser)]
37#[command(
38 name = "obj",
39 about = "Command-line tools for `obj` — embedded document database.",
40 version,
41 propagate_version = true
42)]
43struct Cli {
44 #[command(subcommand)]
45 command: Command,
46}
47
48/// The set of subcommands the CLI accepts.
49///
50/// M12 lands the full quartet: `check`, `stat`, `dump`, `backup`.
51#[derive(Debug, Subcommand)]
52enum Command {
53 /// Run the full bidirectional integrity check on a database
54 /// file and report success / failure.
55 Check {
56 /// Path to the `.obj` file to check.
57 path: PathBuf,
58 },
59 /// Print a structured summary of the file: header info, page
60 /// counts, and per-collection statistics.
61 Stat {
62 /// Path to the `.obj` file to inspect.
63 path: PathBuf,
64 },
65 /// Walk a collection's primary tree and print one record per
66 /// step. The default `--format header` prints the per-doc
67 /// header struct; `--format hex` adds the raw payload bytes
68 /// in hex. No schema-aware decode is performed (the CLI does
69 /// not know about user-defined `Document` types).
70 Dump {
71 /// Path to the `.obj` file to inspect.
72 path: PathBuf,
73 /// Name of the collection to walk.
74 #[arg(long)]
75 collection: String,
76 /// Maximum number of documents to print. `0` = unbounded.
77 #[arg(long, default_value_t = 20)]
78 limit: usize,
79 /// Output format. `header` (default) prints the per-doc
80 /// header struct; `hex` adds the payload bytes in hex.
81 #[arg(long, value_enum, default_value_t = DumpFormat::Header)]
82 format: DumpFormat,
83 },
84 /// Take a hot backup of `src` to `dest`. Writers against
85 /// `src` continue uninterrupted. The destination file MUST
86 /// NOT already exist.
87 Backup {
88 /// Path to the source `.obj` file.
89 src: PathBuf,
90 /// Path the backup will be written to.
91 dest: PathBuf,
92 },
93}
94
95/// Output format for `obj dump`.
96#[derive(Debug, Clone, Copy, ValueEnum)]
97pub(crate) enum DumpFormat {
98 /// Per-doc header only — collection id, type version, payload
99 /// length, payload CRC32C — plus the primary id.
100 Header,
101 /// Same as `header`, with the payload bytes appended as hex.
102 Hex,
103}
104
105/// Parse `std::env::args()` and dispatch into the matched
106/// subcommand. Returns the process exit code the binary should
107/// propagate to the OS.
108///
109/// On argument-parse failure clap exits the process with code 2 by
110/// default; we override [`clap::Error::exit_code`] indirectly via
111/// the [`Cli::try_parse`] path so the binary's `main` can translate
112/// it into the canonical "3 = argument error" exit-code documented
113/// at the top of this module.
114///
115/// Power-of-ten Rule 4 — `run` itself is short; each subcommand
116/// handler lives in its own helper. Rule 7 — every fallible call
117/// is matched or `?`-propagated; no `unwrap` on the path.
118#[must_use]
119pub fn run() -> i32 {
120 let cli = match Cli::try_parse() {
121 Ok(c) => c,
122 Err(err) => {
123 // Surface clap's formatted error then collapse its
124 // varied internal exit-code conventions onto the
125 // CLI-level "3 = arg failure" code.
126 // `err.print()` only fails if writing to stderr fails;
127 // ignore that failure path (`Result` is documented at
128 // the trait — power-of-ten Rule 7).
129 let _ = err.print();
130 return 3;
131 }
132 };
133 match cli.command {
134 Command::Check { path } => check::run(&path),
135 Command::Stat { path } => stat::run(&path),
136 Command::Dump {
137 path,
138 collection,
139 limit,
140 format,
141 } => dump::run(&path, &collection, limit, format),
142 Command::Backup { src, dest } => backup::run(&src, &dest),
143 }
144}