use crate::prelude::{format, String, ToString, Vec};
use crate::schema::namespaces::{ARXIV_DATACITE_REGISTRANT_CODE, DATACITE_DOI_DIRECTORY_INDICATOR, DEFAULT_DOI_SCHEMA_URI};
use crate::schema::pid::{Arxiv, PersistentIdentifier, PersistentIdentifierParse, ISBN};
use crate::util::constants::{RE_DOI, RE_DOI_TEXT};
use crate::util::StringExt;
use crate::util::{regex_capture_lookup, trim_unmatched_trailing_parentheses};
use bon::Builder;
use core::fmt;
#[derive(Builder, Clone, Debug)]
#[builder(start_fn = init, on(String, into))]
pub struct DOI {
pub schema_uri: Option<String>,
pub directory_indicator: Option<String>,
pub registrant_code: Option<String>,
pub suffix: Option<String>,
}
impl Default for DOI {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for DOI {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let result = self.identifier();
write!(f, "{result}")
}
}
impl PersistentIdentifier for DOI {
fn new() -> Self {
DOI::init().build()
}
fn schema_uri(&self) -> String {
self.schema_uri.as_ref().cloned().unwrap_or_default().trim_end_matches("/").to_string()
}
fn identifier(&self) -> String {
let values = [self.prefix(), self.suffix()];
values
.iter()
.flatten()
.filter(|x| !x.is_empty())
.map(String::from)
.collect::<Vec<String>>()
.join("/")
}
fn prefix(&self) -> Option<String> {
let values = [
self.directory_indicator.as_ref().cloned().unwrap_or_default(),
self.registrant_code.as_ref().cloned().unwrap_or_default(),
];
let result = values
.iter()
.filter(|x| !x.is_empty())
.map(String::from)
.collect::<Vec<String>>()
.join(".");
Some(result)
}
fn suffix(&self) -> Option<String> {
fn postprocess(mut value: String) -> String {
if value.ends_with(".") {
value.pop();
}
value
}
let result = self.suffix.as_ref().cloned().unwrap_or_default();
if !result.is_empty() {
Some(postprocess(result))
} else {
None
}
}
fn url(&self) -> String {
let identifier = self.identifier();
if identifier.is_empty() {
String::new()
} else {
let uri = self.schema_uri();
let schema = if uri.is_empty() { DEFAULT_DOI_SCHEMA_URI } else { &uri };
format!("{}/{}", schema, identifier)
}
}
}
impl PersistentIdentifierParse for DOI {
fn find_all(value: impl ToString) -> Vec<Self> {
let re = &RE_DOI;
re.find_iter(&value.to_string())
.filter_map(Result::ok)
.map(|m| DOI::from_string(trim_unmatched_trailing_parentheses(m.as_str())))
.collect()
}
fn format(value: impl ToString) -> String {
DOI::from_string(value).to_string()
}
fn from_string(value: impl ToString) -> Self {
let groups = ["schema_uri", "directory_indicator", "prefix_element", "registrant_code", "suffix"];
let pattern = format!("^{RE_DOI_TEXT}$");
let text = value.to_string().trim().to_string();
let lookup = regex_capture_lookup(pattern.as_ref(), text.as_ref(), groups.to_vec());
DOI::init()
.maybe_schema_uri(lookup.get("schema_uri").cloned())
.maybe_directory_indicator(lookup.get("directory_indicator").cloned())
.maybe_registrant_code(lookup.get("registrant_code").cloned())
.maybe_suffix(lookup.get("suffix").cloned())
.build()
}
fn is_valid(value: impl ToString) -> bool {
let pid = DOI::from_string(value.to_string());
let prefix_is_valid = match pid.prefix() {
| Some(x) => x.replace(".", "").is_numeric() && !x.eq("10.5555"),
| _ => false,
};
let suffix_is_valid = pid.suffix().is_some();
prefix_is_valid && suffix_is_valid
}
}
impl From<ISBN> for DOI {
fn from(isbn: ISBN) -> Self {
DOI::init()
.schema_uri(DEFAULT_DOI_SCHEMA_URI)
.directory_indicator(DATACITE_DOI_DIRECTORY_INDICATOR)
.maybe_registrant_code(isbn.prefix())
.maybe_suffix(isbn.suffix())
.build()
}
}
impl From<Arxiv> for DOI {
fn from(arxiv: Arxiv) -> Self {
let suffix = arxiv.work_identifier().trim_start_matches("arXiv:").to_string();
DOI::init()
.schema_uri(DEFAULT_DOI_SCHEMA_URI)
.directory_indicator(DATACITE_DOI_DIRECTORY_INDICATOR)
.registrant_code(ARXIV_DATACITE_REGISTRANT_CODE)
.suffix(format!("arXiv.{suffix}"))
.build()
}
}
#[cfg(test)]
mod tests;