Skip to main content

doiget_cli/commands/
info.rs

1//! `doiget info <ref>` subcommand — read-only metadata inspection.
2//!
3//! Reads the metadata TOML for a given [`Ref`] from the [`FsStore`] and
4//! prints it on stdout. Network access is never required.
5//!
6//! Per [`docs/PUBLIC_API.md`](../../../../docs/PUBLIC_API.md) §2 the read goes
7//! through the [`Store::read`] trait method, so any future store backend
8//! (e.g. a SQLite index in Phase 2) is a drop-in replacement.
9
10use std::io::Write;
11
12use anyhow::{bail, Context, Result};
13
14use doiget_core::store::{FsStore, Store};
15use doiget_core::Ref;
16
17use super::resolve_store_root;
18
19/// Run the `info` subcommand against the configured store.
20///
21/// `input` is the user-supplied ref string (e.g. `"10.1234/example"`,
22/// `"arxiv:2401.12345"`, or any of the schemes accepted by [`Ref::parse`]).
23///
24/// On success, the entry's [`Metadata`](doiget_core::store::Metadata) is
25/// written to stdout as TOML. On a missing entry, the function returns an
26/// error so the CLI exits non-zero — the caller (a shell pipeline) can
27/// distinguish "entry not in store" from "entry was empty".
28pub fn run(input: String, mode: super::output::OutputMode) -> Result<()> {
29    // `mode` honors ADR-0017: `Quiet` skips emission but still
30    // resolves the entry so the "not-found → exit non-zero" contract
31    // continues to hold (#203). `Json` serialises `Metadata` directly
32    // (it carries `Serialize`); the wire form is the field-name JSON of
33    // the on-disk TOML (#204).
34    let ref_ = Ref::parse(&input).with_context(|| format!("invalid ref: {input}"))?;
35    let safekey = ref_.safekey();
36
37    let store_root = resolve_store_root()?;
38    let store = FsStore::new(store_root)?;
39
40    let metadata = store
41        .read(&safekey)
42        .with_context(|| format!("failed to read store entry for {input}"))?;
43
44    match metadata {
45        Some(m) => {
46            if mode == super::output::OutputMode::Quiet {
47                // Quiet: existence-check only; exit 0 with no stdout.
48                return Ok(());
49            }
50            // Workspace lints deny `print_stdout` (the `print!`/`println!`
51            // macros) so JSON-RPC frames never collide with diagnostics.
52            // `writeln!` / `write!` against an explicit `stdout().lock()`
53            // is the sanctioned escape hatch — the caller chose stdout
54            // explicitly. See `docs/SECURITY.md` §3 / ADR-0001.
55            let stdout = std::io::stdout();
56            let mut out = stdout.lock();
57            if mode == super::output::OutputMode::Json {
58                let s = serde_json::to_string_pretty(&m)
59                    .context("failed to serialize metadata to JSON for stdout")?;
60                writeln!(out, "{s}").context("failed to write metadata JSON to stdout")?;
61                return Ok(());
62            }
63            // Human (default): re-serialize to TOML for stdout. We use
64            // `toml::to_string_pretty` for human readability; this is NOT
65            // the §7-normalized form written to disk, but `info` is a
66            // presentation surface, not a round-tripper.
67            let s = toml::to_string_pretty(&m)
68                .context("failed to serialize metadata to TOML for stdout")?;
69            write!(out, "{s}").context("failed to write metadata to stdout")?;
70            // toml::to_string_pretty already emits a trailing newline, but
71            // be defensive in case a future toml-rs revision changes that.
72            if !s.ends_with('\n') {
73                writeln!(out).context("failed to write trailing newline")?;
74            }
75            Ok(())
76        }
77        None => bail!("no entry for {input}"),
78    }
79}