#[cfg(feature = "std")]
use crate::io::{read_file, write_file, ApiResult, InputOutput};
use crate::prelude::*;
use crate::schema::research_activity::s3a::Status;
use crate::schema::standard::crosswalk::{self, mapping::datacite_to_invenio, CrosswalkError, FieldValue, Fields, SchemaBuilder, SchemaExtractor};
use crate::schema::standard::datacite;
use crate::schema::validate::{is_doi, is_iso_639_language_code};
#[cfg(feature = "std")]
use crate::util::MimeType;
use crate::util::ToProse;
#[cfg(feature = "std")]
use crate::PathBuf;
#[cfg(feature = "std")]
use color_eyre::eyre::eyre;
use core::fmt;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::skip_serializing_none;
use validator::Validate;
pub type Catalog = Vec<Record>;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AccessLevel {
Public,
Restricted,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Access {
pub record: Option<AccessLevel>,
pub files: Option<AccessLevel>,
#[validate(nested)]
pub embargo: Option<Embargo>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct AdditionalDescription {
pub description: String,
#[serde(rename = "type")]
pub kind: Option<DescriptionType>,
#[validate(nested)]
pub lang: Option<LanguageValue>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct AdditionalTitle {
pub title: String,
#[serde(rename = "type")]
pub kind: Option<TitleType>,
#[validate(nested)]
pub lang: Option<LanguageValue>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Affiliation {
pub id: Option<String>,
pub name: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct Award {
pub id: Option<String>,
pub title: Option<Value>,
pub number: Option<String>,
#[validate(nested)]
pub identifiers: Option<Vec<Identifier>>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct CodeMeta {
#[validate(url)]
pub code_repository: Option<String>,
pub programming_language: Option<String>,
pub development_status: Option<Status>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Contributor {
#[validate(nested)]
pub person_or_org: PersonOrOrganization,
#[validate(nested)]
pub role: ControlledVocabularyValue,
#[validate(nested)]
pub affiliations: Option<Vec<Affiliation>>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct ControlledVocabularyValue {
pub id: String,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Creator {
#[validate(nested)]
pub person_or_org: PersonOrOrganization,
#[validate(nested)]
pub role: Option<ControlledVocabularyValue>,
#[validate(nested)]
pub affiliations: Option<Vec<Affiliation>>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct CustomFields {
pub journal: Option<Journal>,
pub imprint: Option<Imprint>,
pub thesis: Option<Thesis>,
pub meeting: Option<Meeting>,
pub codemeta: Option<CodeMeta>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Date {
pub date: String,
#[serde(rename = "type")]
pub kind: DateType,
pub description: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct DateType {
pub id: Option<String>,
pub title: Option<Value>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct DescriptionType {
pub id: Option<String>,
pub title: Option<Value>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Embargo {
pub active: bool,
pub until: Option<String>,
pub reason: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate, PartialEq, Eq)]
pub struct ExternalPersistentIdentifier {
#[validate(custom(function = "is_doi"))]
pub identifier: String,
pub provider: String,
pub client: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct ExternalPersistentIdentifiers {
#[validate(nested)]
pub doi: Option<ExternalPersistentIdentifier>,
#[serde(rename = "concept-doi")]
#[validate(nested)]
pub concept_doi: Option<ExternalPersistentIdentifier>,
#[validate(nested)]
pub handle: Option<ExternalPersistentIdentifier>,
#[validate(nested)]
pub oai: Option<ExternalPersistentIdentifier>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Files {
pub enabled: bool,
pub entries: Option<Value>,
pub default_preview: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Funder {
pub id: Option<String>,
pub name: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct FundingReference {
#[validate(nested)]
pub funder: Funder,
pub award: Option<Award>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Identifier {
pub identifier: String,
pub scheme: String,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Imprint {
pub title: Option<String>,
pub place: Option<String>,
pub pages: Option<String>,
pub isbn: Option<String>,
pub edition: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Journal {
pub title: Option<String>,
pub issn: Option<String>,
pub volume: Option<String>,
pub issue: Option<String>,
pub pages: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Language {
#[validate(custom(function = "is_iso_639_language_code"))]
pub id: String,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct LanguageValue {
#[validate(custom(function = "is_iso_639_language_code"))]
pub id: String,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct LocationFeature {
pub geometry: Option<Value>,
pub identifiers: Option<Vec<LocationIdentifier>>,
pub place: Option<String>,
pub description: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct LocationIdentifier {
pub identifier: String,
pub scheme: String,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct Locations {
pub features: Option<Vec<LocationFeature>>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Meeting {
pub title: Option<String>,
pub acronym: Option<String>,
pub dates: Option<String>,
pub place: Option<String>,
pub session: Option<String>,
pub session_part: Option<String>,
#[serde(rename = "url")]
pub url: Option<String>,
#[validate(nested)]
pub identifiers: Option<Vec<Identifier>>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Metadata {
#[validate(required, nested)]
pub resource_type: Option<ResourceType>,
#[validate(required)]
pub title: Option<String>,
#[validate(required)]
pub publication_date: Option<String>,
#[validate(required, nested)]
pub creators: Option<Vec<Creator>>,
pub additional_titles: Option<Vec<AdditionalTitle>>,
pub description: Option<String>,
pub additional_descriptions: Option<Vec<AdditionalDescription>>,
pub rights: Option<Vec<Right>>,
pub copyright: Option<String>,
#[validate(nested)]
pub contributors: Option<Vec<Contributor>>,
#[validate(nested)]
pub subjects: Option<Vec<Subject>>,
#[validate(nested)]
pub languages: Option<Vec<Language>>,
#[validate(nested)]
pub dates: Option<Vec<Date>>,
pub version: Option<String>,
pub publisher: Option<String>,
#[validate(nested)]
pub identifiers: Option<Vec<Identifier>>,
pub related_identifiers: Option<Vec<RelatedIdentifier>>,
pub sizes: Option<Vec<String>>,
pub formats: Option<Vec<String>>,
pub locations: Option<Locations>,
#[validate(nested)]
pub funding: Option<Vec<FundingReference>>,
#[validate(nested)]
pub references: Option<Vec<Reference>>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Owner {
pub user: Option<i32>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Parent {
pub id: String,
#[validate(nested)]
pub access: Option<ParentAccess>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct ParentAccess {
#[validate(nested)]
pub owned_by: Option<Owner>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate, PartialEq, Eq)]
pub struct PersistentIdentifier {
pub pk: Option<i32>,
pub status: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct PersonOrOrganization {
#[serde(rename = "type")]
pub kind: String,
pub name: Option<String>,
pub given_name: Option<String>,
pub family_name: Option<String>,
#[validate(nested)]
pub identifiers: Option<Vec<PersonOrOrgIdentifier>>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct PersonOrOrgIdentifier {
pub scheme: String,
pub identifier: String,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields)]
pub struct Record {
#[serde(rename = "$schema")]
pub schema: Option<String>,
pub id: Option<String>,
#[validate(nested)]
pub pid: Option<PersistentIdentifier>,
#[validate(nested)]
pub pids: Option<ExternalPersistentIdentifiers>,
#[validate(nested)]
pub parent: Option<Parent>,
#[validate(nested)]
pub access: Option<Access>,
#[validate(nested)]
pub metadata: Option<Metadata>,
pub custom_fields: Option<CustomFields>,
#[validate(nested)]
pub files: Option<Files>,
#[validate(nested)]
pub tombstone: Option<Tombstone>,
pub created: Option<String>,
pub updated: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Reference {
pub reference: String,
pub scheme: Option<String>,
pub identifier: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct RelatedIdentifier {
pub identifier: String,
pub scheme: String,
pub relation_type: Option<RelationType>,
pub resource_type: Option<ResourceTypeInfo>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct RelationType {
pub id: Option<String>,
pub title: Option<Value>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct ResourceType {
pub id: Option<String>,
pub title: Option<Value>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ResourceTypeInfo {
pub id: Option<String>,
pub title: Option<Value>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct Right {
pub id: Option<String>,
pub title: Option<Value>,
pub description: Option<Value>,
pub link: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Subject {
pub id: Option<String>,
pub subject: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Thesis {
pub university: Option<String>,
pub department: Option<String>,
#[serde(rename = "type")]
pub kind: Option<String>,
pub date_submitted: Option<String>,
pub date_defended: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct TitleType {
pub id: Option<String>,
pub title: Option<Value>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
pub struct Tombstone {
pub reason: String,
pub category: String,
#[validate(nested)]
pub removed_by: Owner,
pub timestamp: String,
}
impl fmt::Display for AccessLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
| AccessLevel::Public => write!(f, "public"),
| AccessLevel::Restricted => write!(f, "restricted"),
}
}
}
impl TryFrom<datacite::Record> for Record {
type Error = CrosswalkError;
fn try_from(record: datacite::Record) -> Result<Self, Self::Error> {
let mapping = datacite_to_invenio();
crosswalk::convert(&record, &mapping).map(|(record, _)| record)
}
}
impl TryFrom<&datacite::Record> for Record {
type Error = CrosswalkError;
fn try_from(record: &datacite::Record) -> Result<Self, Self::Error> {
Record::try_from(record.clone())
}
}
impl SchemaBuilder for Record {
fn build_from_fields(fields: &Fields) -> Result<Self, CrosswalkError> {
let identifier_str = fields.get_string("identifier").unwrap_or_default();
let mut metadata = Metadata {
resource_type: None,
title: fields.get_string_opt("title"),
publication_date: fields.get_number_opt("publication-year").map(|n| format!("{:04}-01-01", n as i32)),
creators: None,
additional_titles: None,
description: fields.get_string_opt("description"),
additional_descriptions: None,
rights: None,
copyright: None,
contributors: None,
subjects: None,
languages: None,
dates: None,
version: fields.get_string_opt("version"),
publisher: None,
identifiers: None,
related_identifiers: None,
sizes: None,
formats: None,
locations: None,
funding: None,
references: None,
};
if let Some(creator_names) = fields.get_string_vec_opt("creators") {
metadata.creators = Some(
creator_names
.into_iter()
.map(|name| Creator {
person_or_org: PersonOrOrganization {
kind: "personal".to_string(),
name: Some(name),
given_name: None,
family_name: None,
identifiers: None,
},
affiliations: None,
role: None,
})
.collect(),
);
}
if let Some(rt_str) = fields.get_string_opt("resource-type") {
metadata.resource_type = Some(ResourceType {
id: Some(rt_str),
title: None,
});
}
if let Some(lang_str) = fields.get_string_opt("language") {
metadata.languages = Some(vec![Language { id: lang_str }]);
}
if let Some(ids) = fields.get_string_vec_opt("alternate-identifiers") {
metadata.identifiers = Some(
ids.into_iter()
.map(|identifier| Identifier {
identifier,
scheme: "unknown".to_string(),
})
.collect(),
);
}
if let Some(subjects) = fields.get_string_vec_opt("subjects") {
metadata.subjects = Some(
subjects
.into_iter()
.map(|subject| Subject {
id: None,
subject: Some(subject),
})
.collect(),
);
}
if let Some(license_iri) = fields.get_iri_opt("license") {
metadata.rights = Some(vec![Right {
id: None,
title: None,
description: None,
link: Some(license_iri),
}]);
}
if let Some(contributor_names) = fields.get_string_vec_opt("contributors") {
metadata.contributors = Some(
contributor_names
.into_iter()
.map(|name| Contributor {
person_or_org: PersonOrOrganization {
kind: "personal".to_string(),
name: Some(name),
given_name: None,
family_name: None,
identifiers: None,
},
role: ControlledVocabularyValue {
id: "contributor".to_string(),
},
affiliations: None,
})
.collect(),
);
}
Ok(Record {
schema: Some("https://inveniordm.docs.cern.ch/reference/metadata/".to_string()),
id: Some(identifier_str.clone()),
pid: Some(PersistentIdentifier {
pk: None,
status: Some("U".to_string()),
}),
pids: None,
parent: None,
access: None,
metadata: Some(metadata),
custom_fields: None,
files: None,
tombstone: None,
created: fields.get_date_opt("created"),
updated: fields.get_date_opt("updated"),
})
}
}
impl SchemaExtractor for Record {
fn extract_fields(&self) -> Fields {
let mut fields = Fields::new();
if let Some(pid) = &self.pid {
if let Some(pk) = pid.pk {
fields.insert("identifier", FieldValue::String(pk.to_string()));
}
}
let has_id = fields.contains_key("identifier");
if !has_id {
if let Some(id) = &self.id {
fields.insert("identifier", FieldValue::String(id.clone()));
}
}
if let Some(metadata) = &self.metadata {
if let Some(title) = &metadata.title {
fields.insert("title", FieldValue::String(title.clone()));
}
if let Some(creators) = &metadata.creators {
if !creators.is_empty() {
let creator_names: Vec<String> = creators.iter().filter_map(|c| c.person_or_org.name.clone()).collect();
if !creator_names.is_empty() {
fields.insert("creators", FieldValue::StringVec(creator_names));
}
}
}
if let Some(pub_date) = &metadata.publication_date {
if let Some(year_str) = pub_date.split('-').next() {
if let Ok(year) = year_str.parse::<f64>() {
fields.insert("publication-year", FieldValue::Number(year));
}
}
}
if let Some(resource_type) = &metadata.resource_type {
if let Some(id) = &resource_type.id {
fields.insert("resource-type", FieldValue::String(id.clone()));
}
}
if let Some(description) = &metadata.description {
fields.insert("description", FieldValue::String(description.clone()));
}
if let Some(languages) = &metadata.languages {
if let Some(first) = languages.first() {
fields.insert("language", FieldValue::String(first.id.clone()));
}
}
if let Some(identifiers) = &metadata.identifiers {
if !identifiers.is_empty() {
let id_strings: Vec<String> = identifiers.iter().map(|id| id.identifier.clone()).collect();
fields.insert("alternate-identifiers", FieldValue::StringVec(id_strings));
}
}
if let Some(subjects) = &metadata.subjects {
if !subjects.is_empty() {
let subject_strings: Vec<String> = subjects.iter().filter_map(|s| s.subject.clone()).collect();
if !subject_strings.is_empty() {
fields.insert("subjects", FieldValue::StringVec(subject_strings));
}
}
}
if let Some(rights) = &metadata.rights {
if let Some(first) = rights.first() {
if let Some(link) = &first.link {
fields.insert("license", FieldValue::IRI(link.clone()));
}
}
}
if let Some(contributors) = &metadata.contributors {
if !contributors.is_empty() {
let contributor_names: Vec<String> = contributors.iter().filter_map(|c| c.person_or_org.name.clone()).collect();
if !contributor_names.is_empty() {
fields.insert("contributors", FieldValue::StringVec(contributor_names));
}
}
}
}
if let Some(created) = &self.created {
fields.insert("created", FieldValue::Date(created.clone()));
}
if let Some(updated) = &self.updated {
fields.insert("updated", FieldValue::Date(updated.clone()));
}
fields
}
}
impl ToProse for Record {
fn to_prose(&self) -> String {
self.metadata
.iter()
.flat_map(|metadata| metadata.title.iter().cloned())
.chain(self.metadata.iter().flat_map(|metadata| metadata.description.iter().cloned()))
.chain(
self.metadata
.iter()
.flat_map(|metadata| metadata.additional_descriptions.iter().flatten().map(|value| value.description.clone())),
)
.collect::<Vec<String>>()
.join("\n\n")
}
}
#[cfg(feature = "std")]
impl InputOutput for Record {
fn read(path: impl Into<PathBuf>) -> ApiResult<Record> {
let source = path.into();
match MimeType::from(source.display().to_string()) {
| MimeType::Json => Record::read_json(source),
| MimeType::Yaml => Record::read_yaml(source),
| _ => Err(eyre!("Unsupported Invenio data file extension")),
}
}
fn read_json(path: PathBuf) -> ApiResult<Record> {
#[derive(Deserialize)]
#[serde(untagged)]
enum JsonInput {
One(Box<Record>),
Many(Vec<Record>),
}
read_file(path).and_then(|content| {
serde_json::from_str::<JsonInput>(&content)
.map_err(|why| eyre!("Failed to parse JSON Invenio record — {why}"))
.and_then(|value| match value {
| JsonInput::One(record) => Ok(*record),
| JsonInput::Many(records) => match records.len() {
| 1 => records
.into_iter()
.next()
.ok_or_else(|| eyre!("Expected one Invenio record but found none")),
| len => Err(eyre!("Expected one Invenio record but found {len}")),
},
})
})
}
fn read_yaml(path: PathBuf) -> ApiResult<Record> {
#[derive(Deserialize)]
#[serde(untagged)]
enum YamlInput {
One(Box<Record>),
Many(Vec<Record>),
}
read_file(path).and_then(|content| {
serde_norway::from_str::<YamlInput>(&content)
.map_err(|why| eyre!("Failed to parse YAML Invenio record — {why}"))
.and_then(|value| match value {
| YamlInput::One(record) => Ok(*record),
| YamlInput::Many(records) => match records.len() {
| 1 => records
.into_iter()
.next()
.ok_or_else(|| eyre!("Expected one Invenio record but found none")),
| len => Err(eyre!("Expected one Invenio record but found {len}")),
},
})
})
}
fn write(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
let output = path.into();
match MimeType::from(output.display().to_string()) {
| MimeType::Json => self.write_json(output),
| MimeType::Yaml => self.write_yaml(output),
| _ => Err(eyre!("Unsupported Invenio data file extension for writing")),
}
}
fn write_json(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
let output = path.into().with_extension("json");
serde_json::to_string_pretty(self)
.map_err(|why| eyre!("Failed to serialize JSON Invenio record — {why}"))
.and_then(|content| write_file(output, content))
}
fn write_yaml(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
let output = path.into().with_extension("yaml");
serde_norway::to_string(self)
.map_err(|why| eyre!("Failed to serialize YAML Invenio record — {why}"))
.and_then(|content| write_file(output, content))
}
}