use std::io::Write;
use anyhow::{Context, Result};
use doiget_core::discovery::{resolve_links_for_doi, PaperLinks};
use doiget_core::{ErrorCode, Ref};
use super::fetch::{build_resolve_context, cli_exit_code, render_fetch_error, CliExit};
use super::output::OutputMode;
const OPENALEX_DEFAULT_BASE: &str = "https://api.openalex.org";
pub async fn run(ref_: String, mode: OutputMode, quiet_was_explicit: bool) -> Result<()> {
let parsed = Ref::parse(&ref_).with_context(|| format!("invalid ref {ref_:?}"))?;
let doi = match parsed {
Ref::Doi(d) => d,
Ref::Arxiv(_) => {
anyhow::bail!(
"`doiget link` resolves a DOI to its arXiv preprint; \
arXiv → DOI linking is a follow-up (#281). Pass a DOI."
);
}
};
let base = resolve_openalex_base()?;
let contact_email = std::env::var("DOIGET_CONTACT_EMAIL").unwrap_or_default();
let ctx = build_resolve_context().context("building fetch context")?;
let links = match resolve_links_for_doi(&base, &contact_email, doi.as_str(), &ctx).await {
Ok(l) => l,
Err(e) => {
render_fetch_error(&e);
return Err(anyhow::Error::new(CliExit(cli_exit_code(ErrorCode::from(
&e,
)))));
}
};
if mode == OutputMode::Quiet && quiet_was_explicit {
return Ok(());
}
let stdout = std::io::stdout();
let mut out = stdout.lock();
if mode == OutputMode::Json {
let s = serde_json::to_string_pretty(&links).context("serializing link JSON")?;
writeln!(out, "{s}").context("writing link JSON to stdout")?;
return Ok(());
}
render_human(&mut out, &links)?;
Ok(())
}
fn resolve_openalex_base() -> Result<url::Url> {
let raw =
std::env::var("DOIGET_OPENALEX_BASE").unwrap_or_else(|_| OPENALEX_DEFAULT_BASE.to_string());
url::Url::parse(&raw).with_context(|| format!("DOIGET_OPENALEX_BASE is not a URL: {raw}"))
}
fn render_human(out: &mut impl Write, links: &PaperLinks) -> Result<()> {
let arxiv = match &links.arxiv {
Some(a) => a.as_str(),
None => "- (no arXiv preprint found)",
};
writeln!(out, "doi: {}", links.doi.as_deref().unwrap_or("-"))
.context("writing doi line")?;
writeln!(out, "arxiv: {arxiv}").context("writing arxiv line")?;
writeln!(out, "openalex: {}", links.openalex_id).context("writing openalex line")?;
writeln!(out, "title: {}", links.title).context("writing title line")?;
Ok(())
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
fn sample() -> PaperLinks {
PaperLinks {
doi: Some("10.1103/physrevb.1".into()),
arxiv: Some("2101.54321v2".into()),
openalex_id: "W55".into(),
title: "Published Version".into(),
}
}
#[test]
fn json_output_is_the_paper_links_shape() {
let v = serde_json::to_value(sample()).expect("serialize");
assert_eq!(v["doi"], "10.1103/physrevb.1");
assert_eq!(v["arxiv"], "2101.54321v2");
assert_eq!(v["openalex_id"], "W55");
assert_eq!(v["title"], "Published Version");
}
#[test]
fn human_render_shows_arxiv_and_placeholder() {
let mut buf: Vec<u8> = Vec::new();
render_human(&mut buf, &sample()).expect("render");
let s = String::from_utf8(buf).expect("utf8");
assert!(s.contains("arxiv: 2101.54321v2"), "got: {s}");
let mut none = sample();
none.arxiv = None;
let mut buf2: Vec<u8> = Vec::new();
render_human(&mut buf2, &none).expect("render");
let s2 = String::from_utf8(buf2).expect("utf8");
assert!(s2.contains("no arXiv preprint found"), "got: {s2}");
}
#[tokio::test]
async fn link_rejects_arxiv_input() {
let err = run("arxiv:2401.12345".to_string(), OutputMode::Quiet, true)
.await
.expect_err("arXiv input must be a usage error");
assert!(err.to_string().contains("Pass a DOI"), "got: {err}");
}
}