use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
use crate::registry::{self, Source};
use crate::sources::{Direction, SearchQuery, SortField, SortOrder};
#[derive(Parser)]
#[command(name = "fastpaper", version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[command(flatten)]
pub global: GlobalOpts,
}
#[derive(clap::Args)]
pub struct GlobalOpts {
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(short, long, global = true)]
pub quiet: bool,
#[arg(short, long, global = true, default_value = "table")]
pub format: OutputFormat,
}
#[derive(Subcommand)]
pub enum Commands {
Search(SearchArgs),
Get(GetArgs),
Download(DownloadArgs),
Cite(CiteArgs),
Read(ReadArgs),
Sources(SourcesArgs),
Completions { shell: clap_complete::Shell },
}
#[derive(clap::Args)]
pub struct SearchArgs {
pub source: Source,
pub query: String,
#[arg(short = 'n', long, default_value = "10")]
pub limit: u32,
#[arg(long, default_value = "0")]
pub offset: u32,
#[arg(long)]
pub sort: Option<SortField>,
#[arg(long, default_value = "desc")]
pub order: SortOrder,
#[arg(long)]
pub year: Option<u16>,
#[arg(long)]
pub after: Option<String>,
#[arg(long)]
pub before: Option<String>,
#[arg(long)]
pub author: Option<String>,
#[arg(long)]
pub field: Option<String>,
#[arg(long)]
pub open_access: bool,
#[arg(long)]
pub patents: bool,
#[arg(short, long)]
pub output: Option<PathBuf>,
}
impl SearchArgs {
pub fn to_query(&self) -> SearchQuery {
SearchQuery {
query: self.query.clone(),
limit: self.limit,
offset: self.offset,
sort: self.sort,
order: self.order,
year: self.year,
after: self.after.clone(),
before: self.before.clone(),
author: self.author.clone(),
field: self.field.clone(),
open_access: self.open_access,
patents: self.patents,
}
}
}
#[derive(clap::Args)]
pub struct GetArgs {
#[arg(value_name = "SOURCE_OR_ID")]
pub first: String,
#[arg(value_name = "ID")]
pub second: Option<String>,
}
impl GetArgs {
pub fn resolve(&self) -> Result<(Option<Source>, &str), String> {
resolve_source_and_id(&self.first, self.second.as_deref())
}
}
#[derive(clap::Args)]
pub struct CiteArgs {
#[arg(value_name = "SOURCE_OR_ID")]
pub first: String,
#[arg(value_name = "ID")]
pub second: Option<String>,
#[arg(long, default_value = "incoming")]
pub direction: Direction,
#[arg(short = 'n', long, default_value = "20")]
pub limit: u32,
#[arg(short, long)]
pub output: Option<PathBuf>,
}
impl CiteArgs {
pub fn resolve(&self) -> Result<(Option<Source>, &str), String> {
resolve_source_and_id(&self.first, self.second.as_deref())
}
}
#[derive(clap::Args)]
pub struct DownloadArgs {
#[arg(value_name = "SOURCE_OR_ID")]
pub first: String,
#[arg(value_name = "ID")]
pub second: Option<String>,
#[arg(
short,
long,
env = "FASTPAPER_DOWNLOAD_DIR",
default_value = "./papers"
)]
pub dir: PathBuf,
#[arg(long)]
pub overwrite: bool,
}
impl DownloadArgs {
pub fn resolve(&self) -> Result<(Option<Source>, &str), String> {
resolve_source_and_id(&self.first, self.second.as_deref())
}
}
fn resolve_source_and_id<'a>(
first: &'a str,
second: Option<&'a str>,
) -> Result<(Option<Source>, &'a str), String> {
match second {
None => Ok((None, first)),
Some(id) => match Source::from_name(first) {
Some(source) => Ok((Some(source), id)),
None => Err(format!(
"'{}' is not a known source.\nValid sources: {}",
first,
registry::ALL
.iter()
.map(|s| s.name())
.collect::<Vec<_>>()
.join(", ")
)),
},
}
}
#[derive(clap::Args)]
pub struct ReadArgs {
pub path: PathBuf,
#[arg(long, default_value = "full")]
pub section: Section,
#[arg(long)]
pub max_length: Option<usize>,
#[arg(short, long)]
pub output: Option<PathBuf>,
}
#[derive(clap::Args)]
pub struct SourcesArgs {
#[arg(long)]
pub capabilities: bool,
}
#[derive(ValueEnum, Clone, Copy, Debug)]
pub enum OutputFormat {
Table,
Json,
Jsonl,
Csv,
Bibtex,
}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
pub enum Section {
Abstract,
Introduction,
Methods,
Results,
Discussion,
Conclusion,
References,
Full,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_argument_is_an_identifier() {
let (source, id) = resolve_source_and_id("10.1038/nature12373", None).unwrap();
assert_eq!(source, None);
assert_eq!(id, "10.1038/nature12373");
}
#[test]
fn two_arguments_name_a_source() {
let (source, id) = resolve_source_and_id("arxiv", Some("2301.08745")).unwrap();
assert_eq!(source, Some(Source::Arxiv));
assert_eq!(id, "2301.08745");
}
#[test]
fn unknown_source_name_is_rejected_with_the_valid_list() {
let err = resolve_source_and_id("arxvi", Some("2301.08745")).unwrap_err();
assert!(err.contains("arxvi"), "should quote the bad token: {}", err);
assert!(err.contains("arxiv"), "should list valid sources: {}", err);
}
#[test]
fn one_argument_is_never_treated_as_a_source() {
let (source, id) = resolve_source_and_id("arxiv", None).unwrap();
assert_eq!(source, None);
assert_eq!(id, "arxiv");
}
}