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}