use std::borrow::Cow;
use std::fmt;
use super::IdKind;
use super::syntax::{IdSyntax, impl_id_traits, parse_positive, strip_prefix_ci};
use super::url::Url;
macro_rules! numeric_id {
($(#[$meta:meta])* $name:ident, letter = $letter:literal) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $name(u64);
impl $name {
pub fn new(n: u64) -> Option<Self> {
(n > 0).then_some(Self(n))
}
pub fn get(self) -> u64 {
self.0
}
fn parse_number(s: &str) -> Result<Self, &'static str> {
let digits = strip_prefix_ci(s, $letter).unwrap_or(s);
parse_positive(digits).map(Self)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", $letter, self.0)
}
}
impl_id_traits!($name);
};
}
numeric_id!(
Pmid, letter = ""
);
numeric_id!(
Pmcid, letter = "PMC"
);
numeric_id!(
OpenAlexId, letter = "W"
);
numeric_id!(
CorpusId, letter = ""
);
fn segment_after<'a>(url: &Url<'a>, marker: &str) -> Option<&'a str> {
let mut segs = url.segments();
segs.find(|s| *s == marker)?;
segs.next()
}
impl IdSyntax for Pmid {
const KIND: IdKind = IdKind::Pmid;
fn parse_bare(s: &str) -> Result<Self, &'static str> {
Self::parse_number(s)
}
fn from_url<'a>(url: &Url<'a>) -> Option<Result<Cow<'a, str>, &'static str>> {
let seg = if url.is_host(&["pubmed.ncbi.nlm.nih.gov"]) {
url.segments().next()
} else if url.is_host(&["ncbi.nlm.nih.gov"]) {
segment_after(url, "pubmed")
} else {
None
};
seg.map(|s| Ok(Cow::Borrowed(s)))
}
}
impl IdSyntax for Pmcid {
const KIND: IdKind = IdKind::Pmcid;
fn parse_bare(s: &str) -> Result<Self, &'static str> {
Self::parse_number(s)
}
fn from_url<'a>(url: &Url<'a>) -> Option<Result<Cow<'a, str>, &'static str>> {
let seg = if url.is_host(&["pmc.ncbi.nlm.nih.gov", "ncbi.nlm.nih.gov"]) {
segment_after(url, "articles")
} else {
None
};
seg.map(|s| Ok(Cow::Borrowed(s)))
}
}
impl IdSyntax for OpenAlexId {
const KIND: IdKind = IdKind::OpenAlex;
fn parse_bare(s: &str) -> Result<Self, &'static str> {
Self::parse_number(s)
}
fn from_url<'a>(url: &Url<'a>) -> Option<Result<Cow<'a, str>, &'static str>> {
if !url.is_host(&["openalex.org", "api.openalex.org"]) {
return None;
}
let mut segs = url.segments();
let seg = match segs.next()? {
"works" => segs.next()?,
s => s,
};
seg.starts_with(['W', 'w'])
.then_some(Ok(Cow::Borrowed(seg)))
}
}
impl IdSyntax for CorpusId {
const KIND: IdKind = IdKind::CorpusId;
fn parse_bare(s: &str) -> Result<Self, &'static str> {
Self::parse_number(s)
}
fn from_url<'a>(url: &Url<'a>) -> Option<Result<Cow<'a, str>, &'static str>> {
if !url.is_host(&["api.semanticscholar.org"]) {
return None;
}
let digits = strip_prefix_ci(url.segments().next()?, "corpusid:")?;
Some(Ok(Cow::Borrowed(digits)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::id::IdError;
#[test]
fn parses_and_displays() {
assert_eq!(
"PMID: 0031452104".parse::<Pmid>().unwrap().to_string(),
"31452104"
);
assert_eq!(
"https://www.ncbi.nlm.nih.gov/pubmed/31452104"
.parse::<Pmid>()
.unwrap()
.get(),
31452104
);
assert_eq!(
"pmc6716493".parse::<Pmcid>().unwrap().to_string(),
"PMC6716493"
);
assert_eq!(
"pmcid:6716493".parse::<Pmcid>().unwrap().to_string(),
"PMC6716493"
);
assert_eq!(
"https://pmc.ncbi.nlm.nih.gov/articles/PMC6716493/"
.parse::<Pmcid>()
.unwrap()
.get(),
6716493
);
assert_eq!(
"https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6716493"
.parse::<Pmcid>()
.unwrap()
.get(),
6716493
);
assert_eq!(
"https://api.openalex.org/works/w2741809807"
.parse::<OpenAlexId>()
.unwrap()
.to_string(),
"W2741809807"
);
assert_eq!(
"openalex:2741809807".parse::<OpenAlexId>().unwrap().get(),
2741809807
);
assert_eq!(
"CorpusID:13756489".parse::<CorpusId>().unwrap().get(),
13756489
);
}
#[test]
fn rejects_zero_non_digits_and_overflow() {
for input in ["0", "PMID:abc", "12a", "99999999999999999999999"] {
assert!(
matches!(input.parse::<Pmid>(), Err(IdError::Invalid { .. })),
"{input:?}"
);
}
assert!(
"https://openalex.org/A5023888391"
.parse::<OpenAlexId>()
.is_err()
);
assert_eq!(Pmid::new(0), None);
}
}