use crate::prelude::{format, vec, String, ToString, Vec};
use crate::schema::pid::{noid_check_digit, Betanumeric, PersistentIdentifier, PersistentIdentifierParse};
use crate::util::constants::{RE_ARK, RE_ARK_TEXT};
use crate::util::regex_capture_lookup;
use bon::Builder;
use core::fmt;
#[derive(Builder, Clone, Debug)]
#[builder(start_fn = init, on(String, into))]
pub struct ARK {
pub assigned_name: Option<String>,
#[builder(default = "ark:".to_string())]
pub label: String,
pub name_assigning_authority_number: Option<String>,
pub name_mapping_authority: Option<String>,
#[builder(default = Vec::new())]
pub parts: Vec<String>,
#[builder(default = Vec::new())]
pub variants: Vec<String>,
}
impl Default for ARK {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for ARK {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let nma = self.name_mapping_authority.clone().unwrap_or_default().trim_end_matches('/').to_string();
let identifier = self.identifier();
let result = [nma, identifier].into_iter().filter(|x| !x.is_empty()).collect::<Vec<String>>().join("/");
write!(f, "{result}")
}
}
impl PersistentIdentifier for ARK {
fn new() -> Self {
ARK::init().build()
}
fn schema_uri(&self) -> String {
let uri = match &self.name_mapping_authority {
| Some(value) => value,
| None => "",
};
uri.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> {
match (self.name_assigning_authority_number.as_ref(), self.assigned_name.as_ref()) {
| (Some(naan), Some(name)) => Some(format!("{}{}/{}", self.label.trim_end_matches('/'), naan, name)),
| _ => None,
}
}
fn suffix(&self) -> Option<String> {
let parts = self.parts.join("/");
let variants = self.variants.join(".");
let qualifiers = [parts, variants];
let result = qualifiers
.iter()
.filter(|x| !x.is_empty())
.map(String::from)
.collect::<Vec<String>>()
.join(".");
Some(result)
}
fn check_digit(&self) -> Option<Vec<char>> {
let Self {
name_assigning_authority_number: naan,
assigned_name: name,
..
} = self;
let values = [naan.clone(), name.clone()];
if values.iter().all(|x| x.is_some()) {
let value = values.iter().flatten().map(String::from).collect::<Vec<String>>().join("/");
if value.is_empty() {
None
} else {
let trimmed = value.get(..value.len().saturating_sub(1)).unwrap_or_default();
noid_check_digit(trimmed)
}
} else {
None
}
}
}
impl PersistentIdentifierParse for ARK {
fn find_all(value: impl ToString) -> Vec<Self> {
let re = &RE_ARK;
re.find_iter(&value.to_string())
.filter_map(Result::ok)
.map(|m| ARK::from_string(m.as_str()))
.collect()
}
fn format(value: impl ToString) -> String {
ARK::from_string(value.to_string()).to_string()
}
fn from_string(value: impl ToString) -> Self {
let groups = ["nma", "label", "naan", "assigned_name", "parts", "variants"];
let pattern = format!("^{RE_ARK_TEXT}$");
let text = value.to_string();
let lookup = regex_capture_lookup(pattern.as_ref(), text.as_ref(), groups.to_vec());
let parts = match lookup.get("parts") {
| Some(value) => value.split('/').map(String::from).collect(),
| None => vec![],
};
let variants = match lookup.get("variants") {
| Some(value) => value.split('.').map(String::from).collect(),
| None => vec![],
};
ARK::init()
.maybe_assigned_name(lookup.get("assigned_name").cloned())
.maybe_label(lookup.get("label").cloned())
.maybe_name_assigning_authority_number(lookup.get("naan").cloned())
.maybe_name_mapping_authority(lookup.get("nma").cloned())
.parts(parts)
.variants(variants)
.build()
}
fn is_valid(value: impl ToString) -> bool {
let pid = ARK::from_string(value);
let has_supported_scheme = pid
.name_mapping_authority
.as_ref()
.is_none_or(|authority| authority.starts_with("https://"));
let naan = pid.name_assigning_authority_number.unwrap_or_default();
let naan_is_betanumeric = naan.chars().all(|x| x.is_betanumeric());
let shoulder_starts_with_lowercase_letter = match pid.assigned_name {
| Some(value) => match value.chars().next() {
| Some(value) => value.is_ascii_lowercase() && !value.eq(&'l'),
| None => false,
},
| None => false,
};
has_supported_scheme && !naan.is_empty() && naan_is_betanumeric && shoulder_starts_with_lowercase_letter
}
}
#[cfg(test)]
mod tests;