use crate::prelude::*;
use crate::util::constants::{HTTP_URL, RE_ARXIV};
use crate::util::{base32_crockford_decode, trim_unmatched_trailing_parentheses};
use bon::Builder;
use core::fmt;
#[cfg(feature = "std")]
use data_encoding::HEXLOWER;
#[cfg(feature = "std")]
use ring::digest::{digest, SHA256};
use serde::{Deserialize, Serialize};
use strum::{EnumIs, EnumIter, IntoEnumIterator};
pub mod ark;
pub mod arxiv;
pub mod doi;
pub mod handle;
pub mod isbn;
pub mod isni;
pub mod orcid;
pub mod patent;
pub mod raid;
pub mod ror;
pub mod swhid;
pub use ark::ARK;
pub use arxiv::Arxiv;
pub use doi::DOI;
pub use handle::Handle;
pub use isbn::ISBN;
pub use isni::ISNI;
pub use orcid::ORCID;
pub use patent::Patent;
pub use raid::RAID;
pub use ror::ROR;
pub use swhid::SWHID;
const BETANUMERIC_DIGITS: &str = "0123456789bcdfghjkmnpqrstvwxz";
pub trait Betanumeric {
fn is_betanumeric(&self) -> bool {
false
}
fn to_betanumeric_ordinal(&self) -> Option<usize>;
}
pub trait PersistentIdentifier: fmt::Display {
fn new() -> Self;
fn schema_uri(&self) -> String;
fn identifier(&self) -> String;
fn prefix(&self) -> Option<String> {
None
}
fn suffix(&self) -> Option<String>;
fn check_digit(&self) -> Option<Vec<char>> {
None
}
fn url(&self) -> String {
String::new()
}
}
pub trait PersistentIdentifierConvert<T: AsRef<str>> {
fn format_as(&self, pid_type: PID) -> String;
fn to_pid(&self, pid_type: PID) -> PersistentIdentifierInternal;
fn is_pid(&self, _pid_type: PID) -> bool;
fn is_ark(&self) -> bool;
fn is_arxiv(&self) -> bool;
fn is_doi(&self) -> bool;
fn is_handle(&self) -> bool;
fn is_isbn(&self) -> bool {
false
}
fn is_isni(&self) -> bool;
fn is_orcid(&self) -> bool;
fn is_raid(&self) -> bool;
fn is_ror(&self) -> bool;
fn is_swhid(&self) -> bool;
}
pub trait PersistentIdentifierParse {
fn find_all(value: impl ToString) -> Vec<Self>
where
Self: Sized;
fn format(value: impl ToString) -> String;
fn from_string(value: impl ToString) -> Self
where
Self: Sized;
fn is_valid(value: impl ToString) -> bool;
}
#[derive(Clone, Debug, Default, EnumIs, EnumIter, Eq, Ord, PartialEq, PartialOrd)]
pub enum PID {
#[default]
Unknown,
ARK,
Arxiv,
DOI,
Handle,
ISBN,
ISNI,
ORCID,
Patent,
PIDINST,
RAID,
ROR,
SWHID,
URL,
}
#[derive(Clone, Debug)]
pub enum PublicationIdentifierType {
Doi(DOI),
Arxiv(Arxiv),
Unknown,
}
#[derive(Builder, Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[builder(start_fn = init, on(String, into))]
pub struct Identifier {
pub kind: PID,
pub value: String,
}
#[derive(Default)]
pub struct PersistentIdentifierInternal {
value: String,
pid_type: PID,
}
impl Identifier {
pub fn new(value: impl Into<String>) -> Self {
Self {
kind: PID::Unknown,
value: value.into(),
}
}
pub fn normalized(&self) -> Option<Self> {
let trimmed = self
.value
.trim()
.trim_matches(|character: char| matches!(character, '<' | '>' | '(' | ')' | '[' | ']' | ',' | ';'));
let trimmed = trim_unmatched_trailing_parentheses(trimmed);
match self.kind {
| PID::ARK => Self::parsed::<ARK>(PID::ARK, trimmed),
| PID::Arxiv => Self::parsed::<Arxiv>(PID::Arxiv, trimmed),
| PID::DOI => Self::parsed::<DOI>(PID::DOI, trimmed),
| PID::Handle => Self::parsed::<Handle>(PID::Handle, trimmed),
| PID::ISBN => Self::parsed::<ISBN>(PID::ISBN, trimmed),
| PID::ISNI => Self::parsed::<ISNI>(PID::ISNI, trimmed),
| PID::ORCID => Self::parsed::<ORCID>(PID::ORCID, trimmed),
| PID::Patent => Self::parsed::<Patent>(PID::Patent, trimmed),
| PID::RAID => Self::parsed::<RAID>(PID::RAID, trimmed),
| PID::ROR => Self::parsed::<ROR>(PID::ROR, trimmed),
| PID::SWHID => Self::parsed::<SWHID>(PID::SWHID, trimmed),
| PID::URL if HTTP_URL.is_match(trimmed).unwrap_or(false) => Some(Self {
kind: PID::URL,
value: trimmed.trim_end_matches('/').to_string(),
}),
| PID::Unknown => {
let lowercase = trimmed.to_ascii_lowercase();
let primary = if lowercase.starts_with("raid:") || lowercase.starts_with("https://raid.org/") {
PID::RAID
} else {
PID::DOI
};
[
PID::Arxiv,
primary,
PID::ARK,
PID::Handle,
PID::ISBN,
PID::ORCID,
PID::ISNI,
PID::Patent,
PID::ROR,
PID::SWHID,
PID::URL,
]
.into_iter()
.find_map(|kind| {
Self {
kind,
value: self.value.clone(),
}
.normalized()
})
}
| _ => None,
}
}
pub fn normalize(value: &str) -> String {
let value = value.trim();
match value.split_once(':') {
| Some((prefix, identifier)) => {
let prefix = prefix.to_ascii_lowercase();
let kind = PID::from(prefix.as_str());
let lowercase = kind.is_arxiv() || kind.is_doi() || kind.is_raid() || kind.is_isbn() || kind.is_patent() || kind.is_swhid();
if lowercase {
format!("{prefix}:{}", identifier.trim().to_ascii_lowercase())
} else {
format!("{prefix}:{}", identifier.trim())
}
}
| None => value.to_string(),
}
}
pub fn identity_key(&self) -> String {
match self.kind {
| PID::Arxiv => {
let identifier = Arxiv::from_string(&self.value).work_identifier();
Self::normalize(&format!("arxiv:{}", identifier.trim_start_matches("arXiv:")))
}
| PID::SWHID => Self::normalize(&format!("swhid:{}", SWHID::from_string(&self.value).core_identifier())),
| _ => Self::normalize(&format!("{}:{}", self.kind.as_str(), self.value)),
}
}
fn parsed<T: PersistentIdentifierParse + fmt::Display>(kind: PID, value: &str) -> Option<Self> {
let formatted = T::format(value);
match T::is_valid(&formatted) {
| true => Some(Self { kind, value: formatted }),
| false => T::find_all(value)
.first()
.map(T::format)
.filter(|value| T::is_valid(value))
.map(|value| Self { kind, value }),
}
}
#[cfg(feature = "std")]
pub fn identifier_hash(&self) -> String {
HEXLOWER.encode(digest(&SHA256, self.value.as_bytes()).as_ref())[..12].to_string()
}
}
impl<'a> From<&'a Identifier> for &'a str {
fn from(identifier: &'a Identifier) -> Self {
identifier.kind.as_str()
}
}
impl From<&str> for Identifier {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<ARK> for Identifier {
fn from(value: ARK) -> Self {
Self {
kind: PID::ARK,
value: value.to_string(),
}
}
}
impl From<Arxiv> for Identifier {
fn from(value: Arxiv) -> Self {
Self {
kind: PID::Arxiv,
value: value.to_string(),
}
}
}
impl From<DOI> for Identifier {
fn from(value: DOI) -> Self {
Self {
kind: PID::DOI,
value: value.to_string(),
}
}
}
impl From<Handle> for Identifier {
fn from(value: Handle) -> Self {
Self {
kind: PID::Handle,
value: value.to_string(),
}
}
}
impl From<ISBN> for Identifier {
fn from(value: ISBN) -> Self {
Self {
kind: PID::ISBN,
value: value.to_string(),
}
}
}
impl From<ISNI> for Identifier {
fn from(value: ISNI) -> Self {
Self {
kind: PID::ISNI,
value: value.to_string(),
}
}
}
impl From<ORCID> for Identifier {
fn from(value: ORCID) -> Self {
Self {
kind: PID::ORCID,
value: value.to_string(),
}
}
}
impl From<Patent> for Identifier {
fn from(value: Patent) -> Self {
Self {
kind: PID::Patent,
value: value.to_string(),
}
}
}
impl From<RAID> for Identifier {
fn from(value: RAID) -> Self {
Self {
kind: PID::RAID,
value: value.to_string(),
}
}
}
impl From<ROR> for Identifier {
fn from(value: ROR) -> Self {
Self {
kind: PID::ROR,
value: value.to_string(),
}
}
}
impl From<SWHID> for Identifier {
fn from(value: SWHID) -> Self {
Self {
kind: PID::SWHID,
value: value.to_string(),
}
}
}
impl PID {
pub fn is_discoverable(&self) -> bool {
self.is_ark()
|| self.is_arxiv()
|| self.is_doi()
|| self.is_handle()
|| self.is_isbn()
|| self.is_isni()
|| self.is_orcid()
|| self.is_patent()
|| self.is_raid()
|| self.is_ror()
|| self.is_swhid()
|| self.is_url()
}
pub fn as_str(&self) -> &'static str {
match self {
| Self::ARK => "ark",
| Self::Arxiv => "arxiv",
| Self::DOI => "doi",
| Self::Handle => "handle",
| Self::ISBN => "isbn",
| Self::ISNI => "isni",
| Self::ORCID => "orcid",
| Self::Patent => "patent",
| Self::PIDINST => "pidinst",
| Self::RAID => "raid",
| Self::ROR => "ror",
| Self::SWHID => "swhid",
| Self::URL => "url",
| _ => "unknown",
}
}
pub fn is_project_identifier(&self) -> bool {
self.is_doi() || self.is_arxiv() || self.is_raid() || self.is_isbn() || self.is_patent() || self.is_ark() || self.is_swhid()
}
}
impl fmt::Display for PID {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl Serialize for PID {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for PID {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
String::deserialize(deserializer).and_then(|value| {
Self::iter()
.find(|pid| pid.as_str().eq_ignore_ascii_case(&value))
.ok_or_else(|| serde::de::Error::custom(format!("unknown PID type `{value}`")))
})
}
}
impl From<&str> for PID {
fn from(value: &str) -> Self {
Self::iter()
.find(|pid| pid.as_str().eq_ignore_ascii_case(value.trim()))
.unwrap_or_default()
}
}
impl Betanumeric for char {
fn is_betanumeric(&self) -> bool {
BETANUMERIC_DIGITS.contains(*self)
}
fn to_betanumeric_ordinal(&self) -> Option<usize> {
BETANUMERIC_DIGITS.chars().position(|x| x.eq(self))
}
}
impl<T: AsRef<str>> PersistentIdentifierConvert<T> for T
where
T: ToString,
{
fn format_as(&self, pid_type: PID) -> String {
match pid_type {
| PID::ARK => ARK::format(self.as_ref()),
| PID::Arxiv => Arxiv::format(self.as_ref()),
| PID::DOI => DOI::format(self.as_ref()),
| PID::Handle => Handle::format(self.as_ref()),
| PID::ISBN => ISBN::format(self.as_ref()),
| PID::ISNI => ISNI::format(self.as_ref()),
| PID::ORCID => ORCID::format(self.as_ref()),
| PID::Patent => Patent::format(self.as_ref()),
| PID::RAID => RAID::format(self.as_ref()),
| PID::ROR => <ROR as PersistentIdentifierParse>::format(self.as_ref()),
| PID::SWHID => SWHID::format(self.as_ref()),
| _ => self.as_ref().to_string(),
}
}
fn to_pid(&self, pid_type: PID) -> PersistentIdentifierInternal {
let value = self.as_ref().to_string();
match pid_type {
| PID::ARK => PersistentIdentifierInternal { value, pid_type: PID::ARK },
| PID::Arxiv => PersistentIdentifierInternal { value, pid_type: PID::Arxiv },
| PID::DOI => PersistentIdentifierInternal { value, pid_type: PID::DOI },
| PID::Handle => PersistentIdentifierInternal {
value,
pid_type: PID::Handle,
},
| PID::ISBN => PersistentIdentifierInternal { value, pid_type: PID::ISBN },
| PID::ISNI => PersistentIdentifierInternal { value, pid_type: PID::ISNI },
| PID::ORCID => PersistentIdentifierInternal { value, pid_type: PID::ORCID },
| PID::Patent => PersistentIdentifierInternal {
value,
pid_type: PID::Patent,
},
| PID::RAID => PersistentIdentifierInternal { value, pid_type: PID::RAID },
| PID::ROR => PersistentIdentifierInternal { value, pid_type: PID::ROR },
| PID::SWHID => PersistentIdentifierInternal { value, pid_type: PID::SWHID },
| _ => PersistentIdentifierInternal::default(),
}
}
fn is_pid(&self, pid_type: PID) -> bool {
match pid_type {
| PID::ARK => self.is_ark(),
| PID::Arxiv => self.is_arxiv(),
| PID::DOI => self.is_doi(),
| PID::Handle => self.is_handle(),
| PID::ISBN => self.is_isbn(),
| PID::ISNI => self.is_isni(),
| PID::ORCID => self.is_orcid(),
| PID::Patent => Patent::is_valid(self.as_ref()),
| PID::RAID => self.is_raid(),
| PID::ROR => self.is_ror(),
| PID::SWHID => self.is_swhid(),
| _ => false,
}
}
fn is_ark(&self) -> bool {
ARK::is_valid(self.as_ref())
}
fn is_arxiv(&self) -> bool {
Arxiv::is_valid(self.as_ref())
}
fn is_doi(&self) -> bool {
DOI::is_valid(self.as_ref())
}
fn is_handle(&self) -> bool {
Handle::is_valid(self.as_ref())
}
fn is_isbn(&self) -> bool {
ISBN::is_valid(self.as_ref())
}
fn is_isni(&self) -> bool {
ISNI::is_valid(self.as_ref())
}
fn is_orcid(&self) -> bool {
ORCID::is_valid(self.as_ref())
}
fn is_raid(&self) -> bool {
RAID::is_valid(self.as_ref())
}
fn is_ror(&self) -> bool {
ROR::is_valid(self.as_ref())
}
fn is_swhid(&self) -> bool {
SWHID::is_valid(self.as_ref())
}
}
impl PersistentIdentifierInternal {
pub fn to_ark(&self) -> ARK {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::ARK => ARK::from_string(value),
| _ => ARK::default(),
}
}
pub fn to_arxiv(&self) -> Arxiv {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::Arxiv => Arxiv::from_string(value),
| _ => Arxiv::default(),
}
}
pub fn to_doi(&self) -> DOI {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::DOI => DOI::from_string(value),
| _ => DOI::default(),
}
}
pub fn to_handle(&self) -> Handle {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::Handle => Handle::from_string(value),
| _ => Handle::default(),
}
}
pub fn to_isbn(&self) -> ISBN {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::ISBN => ISBN::from_string(value),
| _ => ISBN::default(),
}
}
pub fn to_orcid(&self) -> ORCID {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::ORCID => ORCID::from_string(value),
| _ => ORCID::default(),
}
}
pub fn to_isni(&self) -> ISNI {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::ISNI => ISNI::from_string(value),
| _ => ISNI::default(),
}
}
pub fn to_patent(&self) -> Patent {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::Patent => Patent::from_string(value),
| _ => Patent::default(),
}
}
pub fn to_raid(&self) -> RAID {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::RAID => RAID::from_string(value),
| _ => RAID::default(),
}
}
pub fn to_ror(&self) -> ROR {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::ROR => ROR::from_string(value),
| _ => ROR::default(),
}
}
pub fn to_swhid(&self) -> SWHID {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::SWHID => SWHID::from_string(value),
| _ => SWHID::default(),
}
}
}
impl From<&str> for PublicationIdentifierType {
fn from(value: &str) -> Self {
let match_covers_value = RE_ARXIV
.find(value)
.ok()
.flatten()
.is_some_and(|matched| matched.start() == 0 && matched.end() == value.len());
let explicitly_labeled_arxiv = match_covers_value && Arxiv::is_valid(value);
match (DOI::is_valid(value), explicitly_labeled_arxiv) {
| (true, _) => Self::Doi(DOI::from_string(value)),
| (_, true) => Self::Arxiv(Arxiv::from_string(value)),
| _ => Self::Unknown,
}
}
}
fn mod_10_or_11_check_digit<S>(value: S) -> Option<Vec<char>>
where
S: AsRef<str>,
{
let working = value
.as_ref()
.chars()
.filter(|character| !matches!(character, '-' | ' '))
.collect::<String>();
match working.len() {
| 10 => working
.chars()
.take(9)
.enumerate()
.try_fold(0_u32, |sum, (index, character)| {
character.to_digit(10).and_then(|digit| {
u32::try_from(index)
.ok()
.and_then(|index| 10_u32.checked_sub(index))
.and_then(|weight| digit.checked_mul(weight))
.and_then(|weighted| sum.checked_add(weighted))
})
})
.and_then(|sum| sum.checked_rem(11))
.and_then(|remainder| 11_u32.checked_sub(remainder))
.and_then(|complement| complement.checked_rem(11))
.and_then(|check_digit| match check_digit {
| 10 => Some(vec!['X']),
| value => char::from_digit(value, 10).map(|character| vec![character]),
}),
| 13 => working
.chars()
.take(12)
.enumerate()
.try_fold(0_u32, |sum, (index, character)| {
character.to_digit(10).and_then(|digit| {
index
.checked_rem(2)
.map(|remainder| if remainder == 0 { 1 } else { 3 })
.and_then(|weight| digit.checked_mul(weight))
.and_then(|weighted| sum.checked_add(weighted))
})
})
.and_then(|sum| sum.checked_rem(10))
.and_then(|remainder| 10_u32.checked_sub(remainder))
.and_then(|complement| complement.checked_rem(10))
.and_then(|check_digit| char::from_digit(check_digit, 10).map(|character| vec![character])),
| _ => None,
}
}
fn mod_11_2_check_digit<S>(value: S) -> Option<Vec<char>>
where
S: AsRef<str>,
{
const COMPLEMENT: u32 = 12;
const MODULUS: u32 = 11;
const RADIX: u32 = 2;
let working = value.as_ref().replace("-", "").replace(" ", "");
let remainder = working.chars().take(15).try_fold(0_u32, |remainder, value| {
let digit = value.to_digit(10).unwrap_or_default();
remainder
.checked_add(digit)
.and_then(|sum| sum.checked_mul(RADIX))
.and_then(|product| product.checked_rem(MODULUS))
});
remainder
.and_then(|remainder| COMPLEMENT.checked_sub(remainder))
.and_then(|value| value.checked_rem(MODULUS))
.and_then(|result| match result {
| 10 => Some(vec!['X']),
| value => char::from_digit(value, 10).map(|value| vec![value]),
})
}
fn mod_97_10_check_digit<S>(value: S) -> Option<Vec<char>>
where
S: AsRef<str>,
{
const COMPLEMENT: u128 = 98;
const MODULUS: u128 = 97;
let working = value
.as_ref()
.replace("-", "")
.replace(" ", "")
.chars()
.take(6)
.map(String::from)
.collect::<Vec<_>>()
.join("");
base32_crockford_decode(working)
.and_then(|value| value.checked_mul(100))
.and_then(|value| value.checked_rem(MODULUS))
.and_then(|remainder| COMPLEMENT.checked_sub(remainder))
.and_then(|complement| complement.checked_rem(MODULUS))
.map(|checksum| format!("{checksum:02}").chars().collect())
}
pub fn noid_check_digit<S>(value: S) -> Option<Vec<char>>
where
S: AsRef<str>,
{
const RADIX: usize = 29;
let remainder = value.as_ref().chars().enumerate().try_fold(0_usize, |acc, (index, value)| {
let ordinal = value.to_betanumeric_ordinal().unwrap_or(0);
index
.checked_rem(RADIX)
.and_then(|position| position.checked_add(1))
.and_then(|position| position.checked_mul(ordinal))
.and_then(|weighted| acc.checked_add(weighted))
.and_then(|sum| sum.checked_rem(RADIX))
});
remainder
.and_then(|value| u8::try_from(value).ok())
.and_then(to_betanumeric)
.map(|value| vec![value])
}
fn to_betanumeric(value: u8) -> Option<char> {
match BETANUMERIC_DIGITS.chars().enumerate().find(|(i, _)| *i == value as usize) {
| Some((_, x)) => Some(x),
| None => None,
}
}
#[cfg(test)]
mod tests;