use std::str::FromStr;
use regex::Regex;
const PROP_PMID: &str = "P698";
const PROP_PMCID: &str = "P932";
const PROP_DOI: &str = "P356";
const PROP_ARXIV: &str = "P818";
const PROP_SEMANTIC_SCHOLAR: &str = "P4011";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum IdProp {
PMID,
PMCID,
DOI,
ARXIV,
SemanticScholar,
}
impl FromStr for IdProp {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_uppercase().trim() {
PROP_PMID => Ok(IdProp::PMID),
PROP_PMCID => Ok(IdProp::PMCID),
PROP_DOI => Ok(IdProp::DOI),
PROP_ARXIV => Ok(IdProp::ARXIV),
PROP_SEMANTIC_SCHOLAR => Ok(IdProp::SemanticScholar),
_ => Err(format!("Invalid ID property: {s}")),
}
}
}
impl std::fmt::Display for IdProp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
IdProp::PMID => PROP_PMID,
IdProp::PMCID => PROP_PMCID,
IdProp::DOI => PROP_DOI,
IdProp::ARXIV => PROP_ARXIV,
IdProp::SemanticScholar => PROP_SEMANTIC_SCHOLAR,
})
}
}
impl IdProp {
pub fn as_str(&self) -> &str {
match self {
IdProp::PMID => PROP_PMID,
IdProp::PMCID => PROP_PMCID,
IdProp::DOI => PROP_DOI,
IdProp::ARXIV => PROP_ARXIV,
IdProp::SemanticScholar => PROP_SEMANTIC_SCHOLAR,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum GenericWorkType {
Property(IdProp),
Item,
}
impl std::fmt::Display for GenericWorkType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
GenericWorkType::Property(prop) => prop.to_string(),
GenericWorkType::Item => "Item".to_string(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GenericWorkIdentifier {
work_type: GenericWorkType,
id: String,
}
impl GenericWorkIdentifier {
pub fn new_prop(prop: IdProp, id: &str) -> Self {
let id = match &prop {
IdProp::DOI => id.to_uppercase(), IdProp::SemanticScholar => id.to_lowercase(),
_other => id.to_string(),
};
Self {
work_type: GenericWorkType::Property(prop),
id,
}
}
pub fn id(&self) -> &str {
&self.id
}
pub fn is_legit(&self) -> bool {
!self.id.is_empty() && self.id != "0"
}
pub fn work_type(&self) -> &GenericWorkType {
&self.work_type
}
pub fn parse_ids_from_str(s: &str) -> Vec<Self> {
lazy_static::lazy_static! {
static ref RE_DOI: Regex = Regex::new(r#"^(.+/.+)$"#).expect("RE_DOI");
static ref RE_PMID: Regex = Regex::new(r#"^(\d+)$"#).expect("RE_PMID");
static ref RE_PMCID: Regex = Regex::new(r#"^(PMC\d+)$"#).expect("RE_PMCID");
}
let mut ids = vec![];
if let Some(x) = RE_DOI.captures(s).and_then(|c| c.get(1)) {
ids.push(Self::new_prop(IdProp::DOI, x.as_str()));
}
if let Some(x) = RE_PMID.captures(s).and_then(|c| c.get(1)) {
ids.push(Self::new_prop(IdProp::PMID, x.as_str()));
}
if let Some(x) = RE_PMCID.captures(s).and_then(|c| c.get(1)) {
ids.push(Self::new_prop(IdProp::PMCID, x.as_str()));
}
ids
}
}
pub fn is_pubmed_id(id: &str) -> bool {
!id.is_empty() && id.chars().all(|c| c.is_ascii_digit())
}
lazy_static::lazy_static! {
static ref RE_QID: Regex = Regex::new(r"^Q\d+$").expect("RE_QID");
static ref RE_PMCID: Regex = Regex::new(r"^PMC\d+$").expect("RE_PMCID");
static ref RE_ORCID: Regex = Regex::new(r"^\d{4}-\d{4}-\d{4}-\d{4}$").expect("RE_ORCID");
}
pub fn is_qid(s: &str) -> bool {
RE_QID.is_match(s)
}
pub fn is_pmcid(s: &str) -> bool {
RE_PMCID.is_match(s)
}
pub fn is_orcid(s: &str) -> bool {
RE_ORCID.is_match(s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_idprop_from_str() {
assert_eq!(IdProp::from_str(PROP_PMID).unwrap(), IdProp::PMID);
assert_eq!(IdProp::from_str(PROP_PMCID).unwrap(), IdProp::PMCID);
assert_eq!(IdProp::from_str(PROP_DOI).unwrap(), IdProp::DOI);
assert_eq!(IdProp::from_str(PROP_ARXIV).unwrap(), IdProp::ARXIV);
assert_eq!(IdProp::from_str(PROP_SEMANTIC_SCHOLAR).unwrap(), IdProp::SemanticScholar);
assert!(IdProp::from_str("P123").is_err());
}
#[test]
fn test_idprop_display() {
assert_eq!(IdProp::PMID.to_string(), PROP_PMID);
assert_eq!(IdProp::PMCID.to_string(), PROP_PMCID);
assert_eq!(IdProp::DOI.to_string(), PROP_DOI);
assert_eq!(IdProp::ARXIV.to_string(), PROP_ARXIV);
assert_eq!(IdProp::SemanticScholar.to_string(), PROP_SEMANTIC_SCHOLAR);
}
#[test]
fn is_qid_accepts_valid_qids() {
assert!(is_qid("Q1"));
assert!(is_qid("Q12345"));
assert!(is_qid("Q9999999"));
}
#[test]
fn is_qid_rejects_invalid() {
assert!(!is_qid(""));
assert!(!is_qid("Q"));
assert!(!is_qid("q12")); assert!(!is_qid("Q12 ")); assert!(!is_qid(" Q12")); assert!(!is_qid("Q12abc")); assert!(!is_qid("P12")); assert!(!is_qid("Q12\nQ34")); }
#[test]
fn is_pmcid_accepts_valid_pmcids() {
assert!(is_pmcid("PMC1"));
assert!(is_pmcid("PMC1234567"));
}
#[test]
fn is_pmcid_rejects_invalid() {
assert!(!is_pmcid(""));
assert!(!is_pmcid("PMC"));
assert!(!is_pmcid("pmc1")); assert!(!is_pmcid("1234567")); assert!(!is_pmcid("PMC12 34"));
}
#[test]
fn is_orcid_accepts_valid_orcid() {
assert!(is_orcid("0000-0001-2345-6789"));
assert!(is_orcid("0000-0000-0000-0000"));
assert!(is_orcid("9999-9999-9999-9999"));
}
#[test]
fn is_orcid_rejects_invalid() {
assert!(!is_orcid(""));
assert!(!is_orcid("0000000123456789")); assert!(!is_orcid("0000-0001-2345")); assert!(!is_orcid("0000-0001-2345-6789-0")); assert!(!is_orcid("000A-0001-2345-6789")); assert!(!is_orcid("https://orcid.org/0000-0001-2345-6789")); }
#[test]
fn test_genericworkidentifier_new_prop() {
let prop = IdProp::DOI;
let id = "10.1234/foobar";
let gwi = GenericWorkIdentifier::new_prop(prop.to_owned(), id);
assert_eq!(gwi.id(), id.to_uppercase());
assert_eq!(gwi.work_type(), &GenericWorkType::Property(prop));
}
#[test]
fn test_genericworkidentifier_is_legit() {
let prop = IdProp::DOI;
let id = "10.1234/foobar";
let gwi = GenericWorkIdentifier::new_prop(prop.to_owned(), id);
assert!(gwi.is_legit());
let gwi = GenericWorkIdentifier::new_prop(prop.to_owned(), "");
assert!(!gwi.is_legit());
let gwi = GenericWorkIdentifier::new_prop(prop.to_owned(), "0");
assert!(!gwi.is_legit());
}
#[test]
fn test_genericworkidentifier_work_type() {
let prop = IdProp::DOI;
let id = "10.1234/foobar";
let gwi = GenericWorkIdentifier::new_prop(prop.to_owned(), id);
assert_eq!(gwi.work_type(), &GenericWorkType::Property(prop));
}
#[test]
fn test_genericworktype_display() {
assert_eq!(GenericWorkType::Item.to_string(), "Item");
assert_eq!(GenericWorkType::Property(IdProp::DOI).to_string(), PROP_DOI);
}
#[test]
fn test_idprop_as_str() {
assert_eq!(IdProp::PMID.as_str(), PROP_PMID);
assert_eq!(IdProp::PMCID.as_str(), PROP_PMCID);
assert_eq!(IdProp::DOI.as_str(), PROP_DOI);
assert_eq!(IdProp::ARXIV.as_str(), PROP_ARXIV);
assert_eq!(IdProp::SemanticScholar.as_str(), PROP_SEMANTIC_SCHOLAR);
}
#[test]
fn parse_ids_from_str_doi() {
let ids = GenericWorkIdentifier::parse_ids_from_str("10.1234/foobar");
assert_eq!(ids.len(), 1);
assert_eq!(ids[0].id(), "10.1234/FOOBAR"); assert_eq!(ids[0].work_type(), &GenericWorkType::Property(IdProp::DOI));
}
#[test]
fn parse_ids_from_str_pmid() {
let ids = GenericWorkIdentifier::parse_ids_from_str("12345678");
assert_eq!(ids.len(), 1);
assert_eq!(ids[0].id(), "12345678");
assert_eq!(ids[0].work_type(), &GenericWorkType::Property(IdProp::PMID));
}
#[test]
fn parse_ids_from_str_pmcid() {
let ids = GenericWorkIdentifier::parse_ids_from_str("PMC12345");
assert_eq!(ids.len(), 1);
assert_eq!(ids[0].id(), "PMC12345");
assert_eq!(ids[0].work_type(), &GenericWorkType::Property(IdProp::PMCID));
}
#[test]
fn parse_ids_from_str_empty_returns_nothing() {
let ids = GenericWorkIdentifier::parse_ids_from_str("");
assert!(ids.is_empty());
}
#[test]
fn parse_ids_from_str_q_item_excluded() {
let ids = GenericWorkIdentifier::parse_ids_from_str("Q12345");
assert!(ids.is_empty());
}
#[test]
fn parse_ids_from_str_doi_is_uppercased() {
let ids = GenericWorkIdentifier::parse_ids_from_str("10.1000/xyz123");
assert!(ids.iter().any(|id| id.id() == "10.1000/XYZ123"));
}
#[test]
fn parse_ids_from_str_doi_with_slash_only() {
let ids = GenericWorkIdentifier::parse_ids_from_str("a/b");
assert!(ids.iter().any(|id| id.work_type() == &GenericWorkType::Property(IdProp::DOI)));
}
}