doiget_cli/commands/
text.rs1use std::io::Write;
21
22use anyhow::{Context, Result};
23
24use doiget_core::paper_text::{paper_text, PaperText, AR5IV_DEFAULT_BASE};
25use doiget_core::{ArxivId, ErrorCode, Ref};
26
27use super::fetch::{build_resolve_context, cli_exit_code, CliExit};
28use super::output::print_err;
29use super::output::OutputMode;
30
31pub async fn run(
42 ref_: String,
43 max_chars: Option<usize>,
44 no_cache: bool,
45 mode: OutputMode,
46 quiet_was_explicit: bool,
47) -> Result<()> {
48 let parsed = super::parse_ref_or_exit(&ref_)?;
49 let id: ArxivId = match parsed {
50 Ref::Arxiv(a) => a,
51 Ref::Doi(_) => {
52 let code = ErrorCode::NoOaAvailable;
56 print_err(format_args!(
57 "error[{}]: no full-text source for a DOI — if an arXiv preprint exists, \
58 pass its id (e.g. `doiget text arxiv:2401.12345`)",
59 code.as_wire()
60 ));
61 return Err(anyhow::Error::new(CliExit(cli_exit_code(code))));
62 }
63 };
64
65 let base = resolve_ar5iv_base()?;
66 let mut ctx = build_resolve_context().context("building fetch context")?;
70 if no_cache {
71 ctx.cache_root = None;
72 }
73
74 let text = match paper_text(&base, &id, max_chars, &ctx).await {
75 Ok(t) => t,
76 Err(e) => {
77 let code = ErrorCode::from(&e);
78 print_err(format_args!("error[{}]: {e}", code.as_wire()));
79 if code == ErrorCode::TextUnavailable {
85 print_err(format_args!(
86 " = note: the arXiv id is valid — fetch the PDF instead: `doiget fetch arxiv:{}`",
87 id.as_str()
88 ));
89 }
90 return Err(anyhow::Error::new(CliExit(cli_exit_code(code))));
91 }
92 };
93
94 if mode == OutputMode::Quiet && quiet_was_explicit {
99 return Ok(());
100 }
101
102 let stdout = std::io::stdout();
103 let mut out = stdout.lock();
104 if mode == OutputMode::Json {
105 let s = serde_json::to_string_pretty(&text).context("serializing paper text JSON")?;
106 writeln!(out, "{s}").context("writing paper text JSON to stdout")?;
107 return Ok(());
108 }
109
110 render_human(&mut out, &text)?;
111 Ok(())
112}
113
114fn resolve_ar5iv_base() -> Result<url::Url> {
117 let raw = std::env::var("DOIGET_AR5IV_BASE").unwrap_or_else(|_| AR5IV_DEFAULT_BASE.to_string());
118 url::Url::parse(&raw).with_context(|| format!("DOIGET_AR5IV_BASE is not a URL: {raw}"))
119}
120
121fn render_human(out: &mut impl Write, text: &PaperText) -> Result<()> {
125 if let Some(t) = &text.title {
126 writeln!(out, "# {t}").context("writing title to stdout")?;
127 }
128 for sec in &text.sections {
129 if let Some(h) = &sec.heading {
130 writeln!(out, "\n## {h}").context("writing section heading to stdout")?;
131 }
132 if !sec.text.is_empty() {
133 writeln!(out, "{}", sec.text).context("writing section body to stdout")?;
134 }
135 }
136 if text.truncated {
137 print_err(format_args!(
138 "note: output truncated to {} chars (raise or drop --max-chars for the full text)",
139 text.char_count
140 ));
141 }
142 Ok(())
143}
144
145#[cfg(test)]
146#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
147mod tests {
148 use super::*;
149 use doiget_core::paper_text::{TextSection, TextSource};
150
151 fn sample() -> PaperText {
152 PaperText {
153 arxiv_id: "2401.12345".into(),
154 source: TextSource::Ar5iv,
155 title: Some("A Title".into()),
156 sections: vec![
157 TextSection {
158 heading: None,
159 text: "Lead paragraph.".into(),
160 },
161 TextSection {
162 heading: Some("1 Introduction".into()),
163 text: "Body text.".into(),
164 },
165 ],
166 char_count: 25,
167 truncated: false,
168 retrieved_from: "https://ar5iv.labs.arxiv.org/html/2401.12345".into(),
169 }
170 }
171
172 #[test]
173 fn json_envelope_is_the_paper_text_shape() {
174 let v = serde_json::to_value(sample()).expect("serialize");
175 assert_eq!(v["arxiv_id"], "2401.12345");
176 assert_eq!(v["source"], "ar5iv");
177 assert_eq!(v["title"], "A Title");
178 assert_eq!(v["sections"][1]["heading"], "1 Introduction");
179 assert_eq!(v["truncated"], false);
180 }
181
182 #[test]
183 fn human_render_lays_out_title_and_sections() {
184 let mut buf: Vec<u8> = Vec::new();
185 render_human(&mut buf, &sample()).expect("render");
186 let s = String::from_utf8(buf).expect("utf8");
187 assert!(s.contains("# A Title"), "got: {s}");
188 assert!(s.contains("## 1 Introduction"), "got: {s}");
189 assert!(s.contains("Lead paragraph."), "got: {s}");
190 assert!(s.contains("Body text."), "got: {s}");
191 }
192
193 #[test]
194 fn resolve_ar5iv_base_defaults_to_production() {
195 if std::env::var("DOIGET_AR5IV_BASE").is_err() {
199 let u = resolve_ar5iv_base().expect("base");
200 assert_eq!(u.as_str(), "https://ar5iv.labs.arxiv.org/");
201 }
202 }
203}