pub mod arxiv;
pub mod biorxiv;
pub mod core;
pub mod crossref;
pub mod dblp;
pub mod doaj;
pub mod europepmc;
pub mod hal;
pub mod medrxiv;
pub mod openaire;
pub mod openalex;
pub mod pmc;
pub mod pubmed;
pub mod scholar;
pub mod semantic;
pub mod unpaywall;
pub mod xueshu;
pub mod zenodo;
use serde::Serialize;
pub fn encode_query(query: &str) -> String {
let mut encoded = String::with_capacity(query.len() * 3);
for byte in query.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
encoded.push(byte as char);
}
b' ' => encoded.push('+'),
_ => {
encoded.push('%');
encoded.push_str(&format!("{:02X}", byte));
}
}
}
encoded
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
pub enum SortField {
Relevance,
Date,
Citations,
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
pub enum SortOrder {
Asc,
Desc,
}
#[derive(Debug, Clone)]
pub struct SearchQuery {
pub query: String,
pub limit: u32,
pub offset: u32,
pub sort: Option<SortField>,
pub order: SortOrder,
pub year: Option<u16>,
pub after: Option<String>,
pub before: Option<String>,
pub author: Option<String>,
pub field: Option<String>,
pub open_access: bool,
pub patents: bool,
}
impl SearchQuery {
pub fn simple(query: &str, limit: u32) -> Self {
SearchQuery {
query: query.to_string(),
limit,
offset: 0,
sort: None,
order: SortOrder::Desc,
year: None,
after: None,
before: None,
author: None,
field: None,
open_access: false,
patents: false,
}
}
pub fn active_filters(&self) -> Vec<&'static str> {
let mut used = Vec::new();
if self.offset > 0 {
used.push("--offset");
}
if self.sort.is_some() {
used.push("--sort");
}
if self.year.is_some() {
used.push("--year");
}
if self.after.is_some() {
used.push("--after");
}
if self.before.is_some() {
used.push("--before");
}
if self.author.is_some() {
used.push("--author");
}
if self.field.is_some() {
used.push("--field");
}
if self.open_access {
used.push("--open-access");
}
if self.patents {
used.push("--patents");
}
used
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SearchCaps {
pub offset: bool,
pub sort: bool,
pub year: bool,
pub date_range: bool,
pub author: bool,
pub field: bool,
pub open_access: bool,
pub patents: bool,
}
impl SearchCaps {
pub const BASIC: SearchCaps = SearchCaps {
offset: false,
sort: false,
year: false,
date_range: false,
author: false,
field: false,
open_access: false,
patents: false,
};
pub fn supports(&self, flag: &str) -> bool {
match flag {
"--offset" => self.offset,
"--sort" => self.sort,
"--year" => self.year,
"--after" | "--before" => self.date_range,
"--author" => self.author,
"--field" => self.field,
"--open-access" => self.open_access,
"--patents" => self.patents,
_ => false,
}
}
pub fn supported_flags(&self) -> Vec<&'static str> {
let mut flags = Vec::new();
if self.offset {
flags.push("--offset");
}
if self.sort {
flags.push("--sort");
}
if self.year {
flags.push("--year");
}
if self.date_range {
flags.push("--after/--before");
}
if self.author {
flags.push("--author");
}
if self.field {
flags.push("--field");
}
if self.open_access {
flags.push("--open-access");
}
if self.patents {
flags.push("--patents");
}
flags
}
}
#[derive(Debug, Clone, Copy)]
pub struct Capabilities {
pub search: Option<SearchCaps>,
pub get: bool,
pub download: bool,
pub cite: bool,
pub max_limit: Option<u32>,
pub notes: &'static str,
}
pub fn validate_ymd(date: &str) -> Result<&str, String> {
let parts: Vec<&str> = date.split('-').collect();
let ok = parts.len() == 3
&& parts[0].len() == 4
&& parts[1].len() == 2
&& parts[2].len() == 2
&& parts.iter().all(|p| p.chars().all(|c| c.is_ascii_digit()));
if ok {
Ok(date)
} else {
Err(format!("Invalid date '{}': expected YYYY-MM-DD", date))
}
}
pub fn contact_email() -> Option<String> {
std::env::var("FASTPAPER_EMAIL")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum Direction {
Incoming,
Outgoing,
}
#[derive(Debug, Clone, Serialize)]
pub struct Paper {
pub id: String,
pub title: String,
pub authors: Vec<String>,
#[serde(rename = "abstract")]
pub abstract_text: Option<String>,
pub year: Option<u16>,
pub doi: Option<String>,
pub url: Option<String>,
pub pdf_url: Option<String>,
pub venue: Option<String>,
pub citations: Option<u32>,
pub fields: Vec<String>,
pub open_access: Option<bool>,
pub source: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_simple_query_asks_for_no_patents() {
assert!(!SearchQuery::simple("attention", 10).patents);
}
#[test]
fn setting_patents_shows_up_as_an_active_filter() {
let mut q = SearchQuery::simple("attention", 10);
q.patents = true;
assert!(q.active_filters().contains(&"--patents"));
}
#[test]
fn patents_is_not_a_basic_capability() {
assert!(!SearchCaps::BASIC.supports("--patents"));
}
#[test]
fn a_source_declaring_patents_supports_the_flag() {
let caps = SearchCaps {
patents: true,
..SearchCaps::BASIC
};
assert!(caps.supports("--patents"));
assert!(caps.supported_flags().contains(&"--patents"));
}
}