doiget_cli/commands/csl.rs
1//! `doiget csl <ref>` — emit a CSL JSON 1.0 array for a stored Metadata.
2//!
3//! Reads the stored [`Metadata`](doiget_core::store::Metadata) for a
4//! [`Ref`] and writes a single-element CSL JSON 1.0 **array** (a drop-in
5//! for citeproc-js / pandoc `--csl-json` consumers that expect a list)
6//! on stdout. Rendering lives in
7//! [`doiget_core::store::render::to_csl_array`] so the CLI and the
8//! `doiget_csl_export` MCP tool share one implementation (Slice 15b).
9//!
10//! Reads go through [`Store::read`]; network access is never required.
11
12use std::io::Write;
13
14use anyhow::{bail, Context, Result};
15
16use doiget_core::store::{render, FsStore, Store};
17use doiget_core::Ref;
18
19use super::resolve_store_root;
20
21/// Run the `csl` subcommand against the configured store.
22///
23/// `input` is the user-supplied ref string (anything accepted by
24/// [`Ref::parse`]). On success a single-element CSL JSON 1.0 array is
25/// written to stdout; a missing entry returns an error so the CLI exits
26/// non-zero (pipelines can distinguish "not in store" from "empty").
27pub fn run(input: String, _mode: super::output::OutputMode) -> Result<()> {
28 // `_mode` is threaded per ADR-0017 / #144. Csl's JSON output is
29 // product output, not informational; Quiet does NOT suppress it.
30 let ref_ = Ref::parse(&input).with_context(|| format!("invalid ref: {input}"))?;
31 let safekey = ref_.safekey();
32
33 let store_root = resolve_store_root()?;
34 let store = FsStore::new(store_root)?;
35
36 let metadata = store
37 .read(&safekey)
38 .with_context(|| format!("failed to read store entry for {input}"))?;
39
40 let metadata = match metadata {
41 Some(m) => m,
42 None => bail!("no entry for {input}"),
43 };
44
45 let array = render::to_csl_array(safekey.as_str(), &metadata);
46 let json =
47 serde_json::to_string_pretty(&array).context("failed to serialize CSL JSON for stdout")?;
48
49 // Workspace lints deny `print_stdout` (`println!`/`print!`); the
50 // sanctioned escape hatch is `writeln!(stdout().lock(), ...)`.
51 // See `docs/SECURITY.md` §3 / ADR-0001 — guarantees JSON-RPC frames
52 // never collide with diagnostics.
53 let stdout = std::io::stdout();
54 let mut out = stdout.lock();
55 writeln!(out, "{json}").context("failed to write CSL JSON to stdout")?;
56 Ok(())
57}