use std::io::Write;
use anyhow::{bail, Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use serde_json::Value;
use doiget_core::refs::{self, Format};
use doiget_core::store::{render, FsStore, Metadata, Store};
use doiget_core::{Ref, Safekey};
use super::fetch::CliExit;
use super::resolve_store_root;
#[allow(clippy::print_stderr)]
fn print_err(args: std::fmt::Arguments<'_>) {
eprintln!("{args}");
}
pub fn run(
ref_: Option<String>,
all: bool,
from_file: Option<Utf8PathBuf>,
_mode: super::output::OutputMode,
) -> Result<()> {
let selectors =
usize::from(ref_.is_some()) + usize::from(all) + usize::from(from_file.is_some());
if selectors > 1 {
bail!("`--all`, `--from-file`, and a positional ref are mutually exclusive");
}
if selectors == 0 {
bail!("specify a ref, `--all`, or `--from-file <FILE>`");
}
let store = FsStore::new(resolve_store_root()?)?;
if all {
return run_all(&store);
}
if let Some(path) = from_file {
return run_from_file(&store, &path);
}
let input = match ref_ {
Some(r) => r,
None => bail!("specify a ref, `--all`, or `--from-file <FILE>`"),
};
let ref_ = Ref::parse(&input).with_context(|| format!("invalid ref: {input}"))?;
let safekey = ref_.safekey();
match store.read(&safekey)? {
Some(m) => write_array(&render::to_csl_array(safekey.as_str(), &m)),
None => bail!("no entry for {input}"),
}
}
fn run_all(store: &FsStore) -> Result<()> {
let entries = store
.list_recent(usize::MAX)
.context("failed to enumerate the store")?;
let mut items: Vec<Value> = Vec::new();
let mut seen: Vec<String> = Vec::new();
for e in &entries {
match store.read(&e.safekey) {
Ok(Some(m)) => push_item(&mut items, &mut seen, &e.safekey, &m),
Ok(None) => {}
Err(err) => print_err(format_args!(
"csl --all: skipping {} (read failed: {err})",
e.safekey.as_str()
)),
}
}
write_array(&Value::Array(items))?;
print_err(format_args!("csl --all: exported {} entries", seen.len()));
Ok(())
}
fn run_from_file(store: &FsStore, path: &Utf8Path) -> Result<()> {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("reading --from-file list: {path}"))?;
let parsed = refs::parse_input(&raw, Format::Auto, Some(path));
let mut items: Vec<Value> = Vec::new();
let mut seen: Vec<String> = Vec::new();
let mut missing = 0usize;
for entry in parsed {
let ref_ = match entry {
Ok(p) => p.ref_,
Err(e) => {
missing += 1;
print_err(format_args!(
"csl --from-file: skipping unparsable entry ({e})"
));
continue;
}
};
let safekey = ref_.safekey();
match store.read(&safekey) {
Ok(Some(m)) => push_item(&mut items, &mut seen, &safekey, &m),
Ok(None) => {
missing += 1;
print_err(format_args!(
"csl --from-file: no store entry for {} (skipped)",
ref_.as_input_str()
));
}
Err(err) => {
missing += 1;
print_err(format_args!(
"csl --from-file: read failed for {} ({err}; skipped)",
ref_.as_input_str()
));
}
}
}
write_array(&Value::Array(items))?;
print_err(format_args!(
"csl --from-file: exported {} entries, {missing} missing",
seen.len()
));
if missing > 0 {
let code = missing.min(255) as i32;
return Err(anyhow::Error::new(CliExit(code)));
}
Ok(())
}
fn push_item(items: &mut Vec<Value>, seen: &mut Vec<String>, safekey: &Safekey, m: &Metadata) {
let key = safekey.as_str();
if seen.iter().any(|k| k == key) {
return;
}
seen.push(key.to_string());
match render::to_csl_array(key, m) {
Value::Array(rendered) if !rendered.is_empty() => items.extend(rendered),
other => tracing::error!(
safekey = key,
value = %other,
"to_csl_array produced no CSL item; entry omitted from export"
),
}
}
fn write_array(array: &Value) -> Result<()> {
let json =
serde_json::to_string_pretty(array).context("failed to serialize CSL JSON for stdout")?;
let stdout = std::io::stdout();
let mut out = stdout.lock();
writeln!(out, "{json}").context("failed to write CSL JSON to stdout")
}