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                                                |
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}