1use clap::{Parser, Subcommand, ValueEnum};
2use std::path::PathBuf;
3
4use crate::registry::{self, Source};
5use crate::sources::{Direction, SearchQuery, SortField, SortOrder};
6
7#[derive(Parser)]
9#[command(name = "fastpaper", version, about, long_about = None)]
10pub struct Cli {
11 #[command(subcommand)]
12 pub command: Commands,
13
14 #[command(flatten)]
15 pub global: GlobalOpts,
16}
17
18#[derive(clap::Args)]
19pub struct GlobalOpts {
20 #[arg(short, long, global = true, action = clap::ArgAction::Count)]
22 pub verbose: u8,
23
24 #[arg(short, long, global = true)]
26 pub quiet: bool,
27
28 #[arg(short, long, global = true, default_value = "table")]
30 pub format: OutputFormat,
31}
32
33#[derive(Subcommand)]
37pub enum Commands {
38 Search(SearchArgs),
40
41 Get(GetArgs),
43
44 Download(DownloadArgs),
46
47 Cite(CiteArgs),
49
50 Read(ReadArgs),
52
53 Sources(SourcesArgs),
55
56 Completions { shell: clap_complete::Shell },
58}
59
60#[derive(clap::Args)]
63pub struct SearchArgs {
64 pub source: Source,
66
67 pub query: String,
69
70 #[arg(short = 'n', long, default_value = "10")]
72 pub limit: u32,
73
74 #[arg(long, default_value = "0")]
76 pub offset: u32,
77
78 #[arg(long)]
80 pub sort: Option<SortField>,
81
82 #[arg(long, default_value = "desc")]
84 pub order: SortOrder,
85
86 #[arg(long)]
88 pub year: Option<u16>,
89
90 #[arg(long)]
92 pub after: Option<String>,
93
94 #[arg(long)]
96 pub before: Option<String>,
97
98 #[arg(long)]
100 pub author: Option<String>,
101
102 #[arg(long)]
104 pub field: Option<String>,
105
106 #[arg(long)]
108 pub open_access: bool,
109
110 #[arg(long)]
112 pub patents: bool,
113
114 #[arg(short, long)]
116 pub output: Option<PathBuf>,
117}
118
119impl SearchArgs {
120 pub fn to_query(&self) -> SearchQuery {
121 SearchQuery {
122 query: self.query.clone(),
123 limit: self.limit,
124 offset: self.offset,
125 sort: self.sort,
126 order: self.order,
127 year: self.year,
128 after: self.after.clone(),
129 before: self.before.clone(),
130 author: self.author.clone(),
131 field: self.field.clone(),
132 open_access: self.open_access,
133 patents: self.patents,
134 }
135 }
136}
137
138#[derive(clap::Args)]
141pub struct GetArgs {
142 #[arg(value_name = "SOURCE_OR_ID")]
144 pub first: String,
145
146 #[arg(value_name = "ID")]
148 pub second: Option<String>,
149}
150
151impl GetArgs {
152 pub fn resolve(&self) -> Result<(Option<Source>, &str), String> {
153 resolve_source_and_id(&self.first, self.second.as_deref())
154 }
155}
156
157#[derive(clap::Args)]
160pub struct CiteArgs {
161 #[arg(value_name = "SOURCE_OR_ID")]
163 pub first: String,
164
165 #[arg(value_name = "ID")]
167 pub second: Option<String>,
168
169 #[arg(long, default_value = "incoming")]
171 pub direction: Direction,
172
173 #[arg(short = 'n', long, default_value = "20")]
175 pub limit: u32,
176
177 #[arg(short, long)]
179 pub output: Option<PathBuf>,
180}
181
182impl CiteArgs {
183 pub fn resolve(&self) -> Result<(Option<Source>, &str), String> {
184 resolve_source_and_id(&self.first, self.second.as_deref())
185 }
186}
187
188#[derive(clap::Args)]
191pub struct DownloadArgs {
192 #[arg(value_name = "SOURCE_OR_ID")]
194 pub first: String,
195
196 #[arg(value_name = "ID")]
198 pub second: Option<String>,
199
200 #[arg(
202 short,
203 long,
204 env = "FASTPAPER_DOWNLOAD_DIR",
205 default_value = "./papers"
206 )]
207 pub dir: PathBuf,
208
209 #[arg(long)]
211 pub overwrite: bool,
212}
213
214impl DownloadArgs {
215 pub fn resolve(&self) -> Result<(Option<Source>, &str), String> {
216 resolve_source_and_id(&self.first, self.second.as_deref())
217 }
218}
219
220fn resolve_source_and_id<'a>(
227 first: &'a str,
228 second: Option<&'a str>,
229) -> Result<(Option<Source>, &'a str), String> {
230 match second {
231 None => Ok((None, first)),
232 Some(id) => match Source::from_name(first) {
233 Some(source) => Ok((Some(source), id)),
234 None => Err(format!(
235 "'{}' is not a known source.\nValid sources: {}",
236 first,
237 registry::ALL
238 .iter()
239 .map(|s| s.name())
240 .collect::<Vec<_>>()
241 .join(", ")
242 )),
243 },
244 }
245}
246
247#[derive(clap::Args)]
250pub struct ReadArgs {
251 pub path: PathBuf,
253
254 #[arg(long, default_value = "full")]
256 pub section: Section,
257
258 #[arg(long)]
260 pub max_length: Option<usize>,
261
262 #[arg(short, long)]
264 pub output: Option<PathBuf>,
265}
266
267#[derive(clap::Args)]
270pub struct SourcesArgs {
271 #[arg(long)]
273 pub capabilities: bool,
274}
275
276#[derive(ValueEnum, Clone, Copy, Debug)]
279pub enum OutputFormat {
280 Table,
281 Json,
282 Jsonl,
283 Csv,
284 Bibtex,
285}
286
287#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
288pub enum Section {
289 Abstract,
290 Introduction,
291 Methods,
292 Results,
293 Discussion,
294 Conclusion,
295 References,
296 Full,
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn one_argument_is_an_identifier() {
305 let (source, id) = resolve_source_and_id("10.1038/nature12373", None).unwrap();
306 assert_eq!(source, None);
307 assert_eq!(id, "10.1038/nature12373");
308 }
309
310 #[test]
311 fn two_arguments_name_a_source() {
312 let (source, id) = resolve_source_and_id("arxiv", Some("2301.08745")).unwrap();
313 assert_eq!(source, Some(Source::Arxiv));
314 assert_eq!(id, "2301.08745");
315 }
316
317 #[test]
318 fn unknown_source_name_is_rejected_with_the_valid_list() {
319 let err = resolve_source_and_id("arxvi", Some("2301.08745")).unwrap_err();
320 assert!(err.contains("arxvi"), "should quote the bad token: {}", err);
321 assert!(err.contains("arxiv"), "should list valid sources: {}", err);
322 }
323
324 #[test]
327 fn one_argument_is_never_treated_as_a_source() {
328 let (source, id) = resolve_source_and_id("arxiv", None).unwrap();
329 assert_eq!(source, None);
330 assert_eq!(id, "arxiv");
331 }
332}