use super::CrosswalkError;
use crate::prelude::HashMap;
use crate::prelude::*;
use core::fmt;
use serde_json::Value;
pub type FieldTransform = fn(FieldValue) -> Result<FieldValue, String>;
#[derive(Clone, Debug)]
pub enum FieldValue {
String(String),
StringVec(Vec<String>),
Date(String),
IRI(String),
Number(f64),
Object(Box<Fields>),
ObjectVec(Vec<Fields>),
Json(Value),
}
#[derive(Clone)]
pub struct FieldMapping {
pub source: &'static str,
pub target: &'static str,
pub rules: Vec<FieldRule>,
}
#[derive(Clone)]
pub struct FieldRule {
pub source: &'static str,
pub target: &'static str,
pub transform: Option<FieldTransform>,
pub required: bool,
}
impl From<HashMap<String, FieldValue>> for Fields {
fn from(fields: HashMap<String, FieldValue>) -> Self {
Self { fields }
}
}
impl Fields {
pub fn new() -> Self {
Self { fields: HashMap::new() }
}
pub fn insert(&mut self, key: impl Into<String>, value: FieldValue) {
self.fields.insert(key.into(), value);
}
pub fn get(&self, key: &str) -> Option<&FieldValue> {
self.fields.get(key)
}
pub fn remove(&mut self, key: &str) -> Option<FieldValue> {
self.fields.remove(key)
}
pub fn contains_key(&self, key: &str) -> bool {
self.fields.contains_key(key)
}
pub fn keys(&self) -> impl Iterator<Item = &String> {
self.fields.keys()
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &FieldValue)> {
self.fields.iter()
}
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
pub fn len(&self) -> usize {
self.fields.len()
}
pub fn get_string(&self, key: &str) -> Result<String, String> {
self.get(key)
.and_then(|v| v.as_string().map(|s| s.to_string()))
.ok_or_else(|| format!("expected string field '{key}'"))
}
pub fn get_string_opt(&self, key: &str) -> Option<String> {
self.get(key).and_then(|v| v.as_string().map(|s| s.to_string()))
}
pub fn get_string_vec(&self, key: &str) -> Result<Vec<String>, String> {
self.get(key)
.and_then(|v| v.as_string_vec().map(|sv| sv.to_vec()))
.ok_or_else(|| format!("expected string vector field '{key}'"))
}
pub fn get_string_vec_opt(&self, key: &str) -> Option<Vec<String>> {
self.get(key).and_then(|v| v.as_string_vec().map(|sv| sv.to_vec()))
}
pub fn get_date(&self, key: &str) -> Result<String, String> {
self.get(key)
.and_then(|v| v.as_date().map(|d| d.to_string()))
.ok_or_else(|| format!("expected date field '{key}'"))
}
pub fn get_date_opt(&self, key: &str) -> Option<String> {
self.get(key).and_then(|v| v.as_date().map(|d| d.to_string()))
}
pub fn get_iri(&self, key: &str) -> Result<String, String> {
self.get(key)
.and_then(|v| v.as_iri().map(|iri| iri.to_string()))
.ok_or_else(|| format!("expected IRI field '{key}'"))
}
pub fn get_iri_opt(&self, key: &str) -> Option<String> {
self.get(key).and_then(|v| v.as_iri().map(|iri| iri.to_string()))
}
pub fn get_number(&self, key: &str) -> Result<f64, String> {
self.get(key)
.and_then(|v| v.as_number())
.ok_or_else(|| format!("expected number field '{key}'"))
}
pub fn get_number_opt(&self, key: &str) -> Option<f64> {
self.get(key).and_then(|v| v.as_number())
}
pub fn get_object(&self, key: &str) -> Result<&Fields, String> {
self.get(key)
.and_then(|v| v.as_object())
.ok_or_else(|| format!("expected object field '{key}'"))
}
pub fn get_object_vec(&self, key: &str) -> Result<&[Fields], String> {
self.get(key)
.and_then(|v| v.as_object_vec())
.ok_or_else(|| format!("expected object vector field '{key}'"))
}
}
#[derive(Clone, Debug, Default)]
pub struct Fields {
fields: HashMap<String, FieldValue>,
}
impl FieldMapping {
pub fn new(source: &'static str, target: &'static str) -> Self {
Self {
source,
target,
rules: Vec::new(),
}
}
pub fn with_rule(mut self, rule: FieldRule) -> Self {
self.rules.push(rule);
self
}
pub fn apply(&self, source: &Fields, target: &mut Fields) -> Result<Vec<String>, CrosswalkError> {
let mut missing = Vec::new();
for rule in &self.rules {
if let Ok(Some(field)) = rule.apply(source, target) {
missing.push(field);
}
}
Ok(missing)
}
}
impl FieldRule {
pub fn new(source: &'static str, target: &'static str) -> Self {
Self {
source,
target,
transform: None,
required: false,
}
}
pub fn required_field(source: &'static str, target: &'static str) -> Self {
Self {
source,
target,
transform: None,
required: true,
}
}
pub fn map(source: &'static str, target: &'static str) -> Self {
Self {
source,
target,
transform: None,
required: false,
}
}
pub fn required(mut self) -> Self {
self.required = true;
self
}
pub fn with_transform(mut self, transform: FieldTransform) -> Self {
self.transform = Some(transform);
self
}
pub fn apply(&self, source: &Fields, target: &mut Fields) -> Result<Option<String>, CrosswalkError> {
match source.get(self.source) {
| Some(value) => {
let transformed = match self.transform {
| Some(f) => f(value.clone()).map_err(|reason| CrosswalkError::TransformationFailed {
field: self.source.to_string(),
reason,
})?,
| None => value.clone(),
};
for target_name in self.target.split('|') {
target.insert(target_name.trim(), transformed.clone());
}
Ok(None)
}
| None => {
if self.required {
Err(CrosswalkError::MissingRequiredField(self.source.to_string()))
} else {
Ok(Some(self.source.to_string()))
}
}
}
}
}
impl fmt::Display for FieldValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
| Self::String(s) => write!(f, "{s}"),
| Self::StringVec(v) => write!(f, "[{}]", v.join(", ")),
| Self::Date(d) => write!(f, "{d}"),
| Self::IRI(iri) => write!(f, "{iri}"),
| Self::Number(n) => write!(f, "{n}"),
| Self::Object(_) => write!(f, "{{...}}"),
| Self::ObjectVec(v) => write!(f, "[...{}]", v.len()),
| Self::Json(val) => write!(f, "{val}"),
}
}
}
impl From<&Fields> for HashMap<String, FieldValue> {
fn from(map: &Fields) -> Self {
map.fields.clone()
}
}
impl FieldValue {
pub fn as_string(&self) -> Option<&str> {
match self {
| Self::String(s) => Some(s),
| _ => None,
}
}
pub fn as_string_vec(&self) -> Option<&[String]> {
match self {
| Self::StringVec(v) => Some(v),
| _ => None,
}
}
pub fn as_date(&self) -> Option<&str> {
match self {
| Self::Date(d) => Some(d),
| _ => None,
}
}
pub fn as_iri(&self) -> Option<&str> {
match self {
| Self::IRI(iri) => Some(iri),
| _ => None,
}
}
pub fn as_number(&self) -> Option<f64> {
match self {
| Self::Number(n) => Some(*n),
| _ => None,
}
}
pub fn as_object(&self) -> Option<&Fields> {
match self {
| Self::Object(map) => Some(map),
| _ => None,
}
}
pub fn as_object_vec(&self) -> Option<&[Fields]> {
match self {
| Self::ObjectVec(v) => Some(v),
| _ => None,
}
}
}
pub fn datacite_to_dcat() -> FieldMapping {
FieldMapping::new("datacite", "dcat")
.with_rule(FieldRule::new("doi", "identifier").required())
.with_rule(FieldRule::new("title", "title"))
.with_rule(FieldRule::new("description", "description"))
.with_rule(FieldRule::new("language", "language").with_transform(as_vec))
.with_rule(FieldRule::new("creators", "creators"))
.with_rule(FieldRule::new("publisher", "publisher"))
.with_rule(FieldRule::new("publication-year", "issued").with_transform(year_to_date))
.with_rule(FieldRule::new("license", "license"))
.with_rule(FieldRule::new("url", "landing_page"))
.with_rule(FieldRule::new("subjects", "keywords"))
.with_rule(FieldRule::new("resource-type", "type_").with_transform(resource_type_to_iri))
}
pub fn datacite_to_huwise() -> FieldMapping {
FieldMapping::new("datacite", "huwise")
.with_rule(FieldRule::map("doi", "identifier").required())
.with_rule(FieldRule::map("title", "title"))
.with_rule(FieldRule::map("description", "description"))
.with_rule(FieldRule::map("creators", "creators"))
.with_rule(FieldRule::map("publication-year", "publication-year"))
.with_rule(FieldRule::map("resource-type", "resource-type"))
.with_rule(FieldRule::map("subjects", "subjects"))
.with_rule(FieldRule::map("language", "language"))
.with_rule(FieldRule::map("publisher", "publisher"))
.with_rule(FieldRule::map("license", "license"))
.with_rule(FieldRule::map("version", "version"))
}
pub fn datacite_to_invenio() -> FieldMapping {
FieldMapping::new("datacite", "invenio")
.with_rule(FieldRule::map("doi", "identifier").required())
.with_rule(FieldRule::map("title", "title"))
.with_rule(FieldRule::map("description", "description"))
.with_rule(FieldRule::map("language", "language"))
.with_rule(FieldRule::map("creators", "creators"))
.with_rule(FieldRule::map("publication-year", "publication-year").with_transform(extract_year))
.with_rule(FieldRule::map("resource-type", "resource-type"))
.with_rule(FieldRule::map("license", "license"))
.with_rule(FieldRule::map("alternate-identifiers", "alternate-identifiers"))
.with_rule(FieldRule::map("subjects", "subjects"))
.with_rule(FieldRule::map("contributors", "contributors"))
.with_rule(FieldRule::map("version", "version"))
}
pub fn dcat_to_datacite() -> FieldMapping {
FieldMapping::new("dcat", "datacite")
.with_rule(FieldRule::new("identifier", "doi").required().with_transform(first_of_vec))
.with_rule(FieldRule::new("title", "title"))
.with_rule(FieldRule::new("description", "description"))
.with_rule(FieldRule::new("language", "language").with_transform(first_of_vec))
.with_rule(FieldRule::new("creators", "creators"))
.with_rule(FieldRule::new("publisher", "publisher"))
.with_rule(FieldRule::new("issued", "publication-year").with_transform(extract_year))
.with_rule(FieldRule::new("license", "license"))
.with_rule(FieldRule::new("type_", "resource-type").with_transform(iri_to_resource_type))
.with_rule(FieldRule::new("keywords", "subjects"))
.with_rule(FieldRule::new("themes", "subjects"))
}
pub fn huwise_to_datacite() -> FieldMapping {
FieldMapping::new("huwise", "datacite")
.with_rule(FieldRule::map("doi", "doi").required())
.with_rule(FieldRule::map("title", "title"))
.with_rule(FieldRule::map("description", "description"))
.with_rule(FieldRule::map("creators", "creators"))
.with_rule(FieldRule::map("contributors", "contributors"))
.with_rule(FieldRule::map("publication-year", "publication-year"))
.with_rule(FieldRule::map("resource-type", "resource-type"))
.with_rule(FieldRule::map("subjects", "subjects"))
.with_rule(FieldRule::map("language", "language"))
.with_rule(FieldRule::map("publisher", "publisher"))
.with_rule(FieldRule::map("license", "license"))
.with_rule(FieldRule::map("version", "version"))
}
pub fn invenio_to_datacite() -> FieldMapping {
FieldMapping::new("invenio", "datacite")
.with_rule(FieldRule::map("identifier", "doi").required())
.with_rule(FieldRule::map("title", "title"))
.with_rule(FieldRule::map("description", "description"))
.with_rule(FieldRule::map("language", "language"))
.with_rule(FieldRule::map("creators", "creators"))
.with_rule(FieldRule::map("publication-year", "publication-year"))
.with_rule(FieldRule::map("resource-type", "resource-type"))
.with_rule(FieldRule::map("license", "license"))
.with_rule(FieldRule::map("alternate-identifiers", "alternate-identifiers"))
.with_rule(FieldRule::map("subjects", "subjects"))
.with_rule(FieldRule::map("contributors", "contributors"))
}
pub fn first_of_vec(value: FieldValue) -> Result<FieldValue, String> {
match value {
| FieldValue::StringVec(mut v) if !v.is_empty() => Ok(FieldValue::String(v.remove(0))),
| FieldValue::StringVec(_) => Err("empty string vector".to_string()),
| _ => Err("expected string vector".to_string()),
}
}
pub fn as_vec(value: FieldValue) -> Result<FieldValue, String> {
match value {
| FieldValue::StringVec(_) => Ok(value),
| FieldValue::String(s) => Ok(FieldValue::StringVec(vec![s])),
| _ => Err("expected string or string vector".to_string()),
}
}
pub fn extract_year(value: FieldValue) -> Result<FieldValue, String> {
match value {
| FieldValue::Date(d) => {
let year = d
.split('-')
.next()
.and_then(|y| y.parse::<i32>().ok())
.ok_or_else(|| format!("cannot extract year from date: {d}"))?;
Ok(FieldValue::Number(year as f64))
}
| FieldValue::String(s) => s
.parse::<i32>()
.map(|y| FieldValue::Number(y as f64))
.map_err(|_| format!("cannot parse year from string: {s}")),
| FieldValue::Number(n) => Ok(FieldValue::Number(n)),
| _ => Err("expected date, string, or number".to_string()),
}
}
pub fn year_to_date(value: FieldValue) -> Result<FieldValue, String> {
match value {
| FieldValue::Number(n) => Ok(FieldValue::Date(format!("{:04}-01-01", n as i32))),
| FieldValue::String(s) => {
let _: i32 = s.parse().map_err(|_| format!("cannot parse year: {s}"))?;
Ok(FieldValue::Date(format!("{s}-01-01")))
}
| _ => Err("expected number or string year".to_string()),
}
}
pub fn resource_type_to_iri(value: FieldValue) -> Result<FieldValue, String> {
match value {
| FieldValue::String(s) => {
let iri = match s.as_str() {
| "Dataset" => "http://www.w3.org/ns/dcat#Dataset",
| "Software" => "https://schema.org/SoftwareApplication",
| "Audiovisual" => "https://schema.org/VideoObject",
| "Book" => "https://schema.org/Book",
| "BookChapter" => "https://schema.org/Chapter",
| "ConferencePaper" => "https://schema.org/ScholarlyArticle",
| "Dissertation" => "https://schema.org/Thesis",
| "Image" => "https://schema.org/ImageObject",
| "Journal" => "https://schema.org/Periodical",
| "JournalArticle" => "https://schema.org/ScholarlyArticle",
| "Report" => "https://schema.org/Report",
| "Text" => "https://schema.org/CreativeWork",
| _ => return Err(format!("unknown resource type: {s}")),
};
Ok(FieldValue::IRI(iri.to_string()))
}
| _ => Err("expected string resource type".to_string()),
}
}
pub fn iri_to_resource_type(value: FieldValue) -> Result<FieldValue, String> {
match value {
| FieldValue::IRI(iri) => {
let type_str = match iri.as_str() {
| "http://www.w3.org/ns/dcat#Dataset" => "Dataset",
| "https://schema.org/SoftwareApplication" => "Software",
| "https://schema.org/VideoObject" => "Audiovisual",
| "https://schema.org/Book" => "Book",
| "https://schema.org/Chapter" => "BookChapter",
| "https://schema.org/ScholarlyArticle" => "JournalArticle",
| "https://schema.org/Thesis" => "Dissertation",
| "https://schema.org/ImageObject" => "Image",
| "https://schema.org/Periodical" => "Journal",
| "https://schema.org/Report" => "Report",
| "https://schema.org/CreativeWork" => "Text",
| _ => return Err(format!("unknown IRI: {iri}")),
};
Ok(FieldValue::String(type_str.to_string()))
}
| _ => Err("expected IRI resource type".to_string()),
}
}