doiget_cli/commands/
csl.rs1use std::io::Write;
15
16use anyhow::{bail, Context, Result};
17use camino::{Utf8Path, Utf8PathBuf};
18use serde_json::Value;
19
20use doiget_core::refs::{self, Format};
21use doiget_core::store::{render, FsStore, Metadata, Store};
22use doiget_core::Safekey;
23
24use super::fetch::CliExit;
25use super::output::print_err;
26use super::resolve_store_root;
27
28pub fn run(
37 ref_: Option<String>,
38 all: bool,
39 from_file: Option<Utf8PathBuf>,
40 _mode: super::output::OutputMode,
41) -> Result<()> {
42 let selectors =
43 usize::from(ref_.is_some()) + usize::from(all) + usize::from(from_file.is_some());
44 if selectors > 1 {
45 bail!("`--all`, `--from-file`, and a positional ref are mutually exclusive");
46 }
47 if selectors == 0 {
51 bail!("specify a ref, `--all`, or `--from-file <FILE>`");
52 }
53
54 let store = FsStore::new(resolve_store_root()?)?;
55
56 if all {
57 return run_all(&store);
58 }
59 if let Some(path) = from_file {
60 return run_from_file(&store, &path);
61 }
62
63 let input = match ref_ {
65 Some(r) => r,
66 None => bail!("specify a ref, `--all`, or `--from-file <FILE>`"),
67 };
68 let ref_ = super::parse_ref_or_exit(&input)?;
69 let safekey = ref_.safekey();
70 match store.read(&safekey)? {
71 Some(m) => write_array(&render::to_csl_array(safekey.as_str(), &m)),
72 None => bail!("no entry for {input}"),
73 }
74}
75
76fn run_all(store: &FsStore) -> Result<()> {
79 let entries = store
80 .list_recent(usize::MAX)
81 .context("failed to enumerate the store")?;
82
83 let mut items: Vec<Value> = Vec::new();
84 let mut seen: Vec<String> = Vec::new();
85 for e in &entries {
86 match store.read(&e.safekey) {
87 Ok(Some(m)) => push_item(&mut items, &mut seen, &e.safekey, &m),
88 Ok(None) => {}
89 Err(err) => print_err(format_args!(
90 "csl --all: skipping {} (read failed: {err})",
91 e.safekey.as_str()
92 )),
93 }
94 }
95
96 write_array(&Value::Array(items))?;
97 print_err(format_args!("csl --all: exported {} entries", seen.len()));
98 Ok(())
99}
100
101fn run_from_file(store: &FsStore, path: &Utf8Path) -> Result<()> {
106 let raw = std::fs::read_to_string(path)
107 .with_context(|| format!("reading --from-file list: {path}"))?;
108 let parsed = refs::parse_input(&raw, Format::Auto, Some(path));
111
112 let mut items: Vec<Value> = Vec::new();
113 let mut seen: Vec<String> = Vec::new();
114 let mut missing = 0usize;
115 for entry in parsed {
116 let ref_ = match entry {
117 Ok(p) => p.ref_,
118 Err(e) => {
119 missing += 1;
120 print_err(format_args!(
121 "csl --from-file: skipping unparsable entry ({e})"
122 ));
123 continue;
124 }
125 };
126 let safekey = ref_.safekey();
127 match store.read(&safekey) {
131 Ok(Some(m)) => push_item(&mut items, &mut seen, &safekey, &m),
132 Ok(None) => {
133 missing += 1;
134 print_err(format_args!(
135 "csl --from-file: no store entry for {} (skipped)",
136 ref_.as_input_str()
137 ));
138 }
139 Err(err) => {
140 missing += 1;
141 print_err(format_args!(
142 "csl --from-file: read failed for {} ({err}; skipped)",
143 ref_.as_input_str()
144 ));
145 }
146 }
147 }
148
149 write_array(&Value::Array(items))?;
150 print_err(format_args!(
151 "csl --from-file: exported {} entries, {missing} missing",
152 seen.len()
153 ));
154 if missing > 0 {
155 let code = missing.min(255) as i32;
156 return Err(anyhow::Error::new(CliExit(code)));
157 }
158 Ok(())
159}
160
161fn push_item(items: &mut Vec<Value>, seen: &mut Vec<String>, safekey: &Safekey, m: &Metadata) {
166 let key = safekey.as_str();
167 if seen.iter().any(|k| k == key) {
168 return;
169 }
170 seen.push(key.to_string());
171 match render::to_csl_array(key, m) {
172 Value::Array(rendered) if !rendered.is_empty() => items.extend(rendered),
173 other => tracing::error!(
178 safekey = key,
179 value = %other,
180 "to_csl_array produced no CSL item; entry omitted from export"
181 ),
182 }
183}
184
185fn write_array(array: &Value) -> Result<()> {
189 let json =
190 serde_json::to_string_pretty(array).context("failed to serialize CSL JSON for stdout")?;
191 let stdout = std::io::stdout();
192 let mut out = stdout.lock();
193 writeln!(out, "{json}").context("failed to write CSL JSON to stdout")
194}