use chrono::NaiveDate;
use clap::{Args, Parser, Subcommand, ValueEnum};
use paper_gap::{Result, analyse, arxiv, pwc, report, repos, sources};
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "paper-gap",
about = "Find research papers with weak or missing public code"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Scan(ScanArgs),
Inspect(InspectArgs),
Report(ReportArgs),
Sources(SourcesArgs),
}
#[derive(Args)]
struct ScanArgs {
#[arg(long)]
query: Option<String>,
#[arg(long)]
category: Option<String>,
#[arg(long)]
since: Option<String>,
#[arg(long)]
from: Option<NaiveDate>,
#[arg(long)]
to: Option<NaiveDate>,
#[arg(long, default_value_t = 25)]
limit: usize,
#[arg(long, value_enum, default_value_t = OutputFormat::Markdown)]
format: OutputFormat,
#[arg(long)]
output: Option<PathBuf>,
}
#[derive(Args)]
struct InspectArgs {
#[arg(long)]
arxiv: String,
#[arg(long, value_enum, default_value_t = OutputFormat::Markdown)]
format: OutputFormat,
#[arg(long)]
output: Option<PathBuf>,
}
#[derive(Args)]
struct ReportArgs {
#[arg(long, default_value = "-")]
input: String,
#[arg(long, value_enum, default_value_t = OutputFormat::Markdown)]
format: OutputFormat,
#[arg(long)]
output: Option<PathBuf>,
}
#[derive(Args)]
struct SourcesArgs {
#[command(subcommand)]
command: Option<SourcesCommand>,
#[arg(long)]
papers: bool,
#[arg(long)]
repos: bool,
#[arg(long)]
check: bool,
}
#[derive(Subcommand)]
enum SourcesCommand {
Discover(DiscoverArgs),
}
#[derive(Args)]
struct DiscoverArgs {
#[arg(long, value_enum, default_value_t = DiscoverKind::All)]
kind: DiscoverKind,
}
#[derive(Clone, ValueEnum)]
enum DiscoverKind {
Papers,
Repos,
All,
}
#[derive(Clone, Copy, ValueEnum)]
enum OutputFormat {
Markdown,
Json,
}
impl From<OutputFormat> for report::OutputFormat {
fn from(value: OutputFormat) -> Self {
match value {
OutputFormat::Markdown => Self::Markdown,
OutputFormat::Json => Self::Json,
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Scan(args) => scan(args).await,
Command::Inspect(args) => inspect(args).await,
Command::Report(args) => report(args),
Command::Sources(args) => sources(args).await,
}
}
async fn scan(args: ScanArgs) -> Result<()> {
let query = arxiv::query_string(&arxiv::PaperQuery {
query: args.query,
category: args.category,
since: args.since,
from: args.from,
to: args.to,
})?;
eprintln!("paper-gap: fetching arXiv papers...");
let papers = arxiv::search(&query, args.limit).await?;
eprintln!(
"paper-gap: found {} paper(s); checking code links and repositories...",
papers.len()
);
let report = build_report(papers).await?;
write_output(&render_report(&report, args.format)?, args.output)
}
async fn inspect(args: InspectArgs) -> Result<()> {
eprintln!("paper-gap: fetching arXiv paper {}...", args.arxiv);
let papers = arxiv::search(&format!("id:{}", args.arxiv), 1).await?;
eprintln!(
"paper-gap: found {} paper(s); checking code links and repositories...",
papers.len()
);
let report = build_report(papers).await?;
write_output(&render_report(&report, args.format)?, args.output)
}
fn report(args: ReportArgs) -> Result<()> {
let input = if args.input == "-" {
let mut s = String::new();
io::stdin().read_to_string(&mut s)?;
s
} else {
fs::read_to_string(&args.input)?
};
let report: report::ScanReport = serde_json::from_str(&input)?;
write_output(&render_report(&report, args.format)?, args.output)
}
async fn sources(args: SourcesArgs) -> Result<()> {
if let Some(SourcesCommand::Discover(args)) = args.command {
let kind = match args.kind {
DiscoverKind::Papers => sources::DiscoverKind::Papers,
DiscoverKind::Repos => sources::DiscoverKind::Repos,
DiscoverKind::All => sources::DiscoverKind::All,
};
println!(
"{}",
serde_json::to_string_pretty(&sources::discover(kind).await?)?
);
return Ok(());
}
let filter = match (args.papers, args.repos) {
(true, false) => sources::SourceFilter::Papers,
(false, true) => sources::SourceFilter::Repos,
_ => sources::SourceFilter::All,
};
let providers = sources::filter(sources::builtin(), filter);
if args.check {
for provider in &providers {
println!(
"{} {} {}",
provider.id,
provider.display_name,
sources::health_check(provider.base_url).await
);
}
} else {
println!("{}", serde_json::to_string_pretty(&providers)?);
}
Ok(())
}
async fn build_report(papers: Vec<arxiv::Paper>) -> Result<report::ScanReport> {
let mut paper_results = Vec::new();
for (index, paper) in papers.into_iter().enumerate() {
eprintln!(
"paper-gap: [{}] checking Papers with Code: {}",
index + 1,
paper.title
);
let code_links = pwc::code_links(&paper).await.unwrap_or_default();
eprintln!("paper-gap: [{}] searching repositories...", index + 1);
let repositories = repos::search_for_paper(&paper, 5).await.unwrap_or_default();
let repositories = analyse::dedup_repositories(repositories);
let gap_assessment = analyse::assess(&code_links, &repositories);
paper_results.push(report::PaperResult {
paper,
code_links,
repositories,
gap_assessment,
});
}
Ok(report::ScanReport { paper_results })
}
fn render_report(report: &report::ScanReport, format: OutputFormat) -> Result<String> {
report::render(report, format.into())
}
fn write_output(text: &str, output: Option<PathBuf>) -> Result<()> {
if let Some(path) = output {
fs::write(path, text)?;
} else {
println!("{}", text);
}
Ok(())
}