#![allow(
clippy::arithmetic_side_effects,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic,
clippy::unwrap_used
)]
use crate::infrastructure::ComputeEnvironment;
use crate::pid::raid::{self, CreditRole};
use crate::research_activity::output::{Award, Contributor, Funding, ResearchOutput};
use crate::research_activity::{
ActivityContributor, ActivityDateType, ActivityOrganization, AgentIdentifier, ControlledSubject, DatedAgentRole, DatedOrganizationRole,
RelatedResourceCategory, ResearchActivity, ResearchActivityMetadata, RightsAccess, StructuredRights, TypedActivityDate, TypedIdentifier,
TypedRelatedResource,
};
use crate::standard::crosswalk::*;
use crate::standard::crosswalk::{
mapping::{
datacite_to_dcat, datacite_to_huwise, datacite_to_invenio, dcat_to_datacite, extract_year, huwise_to_datacite, invenio_to_datacite,
year_to_date,
},
FieldValue, Fields, SchemaBuilder, SchemaExtractor,
};
use crate::standard::{datacite, dcat, huwise, invenio};
use crate::validation::Validate;
use crate::{ContactPoint, OneOrMany};
use alloc::collections::BTreeMap;
use proptest::{
prelude::*,
test_runner::{Config as ProptestConfig, FileFailurePersistence},
};
use serde_json::json;
fn datacite_project_activity() -> ResearchActivity {
let publisher_identifier = TypedIdentifier::init()
.value("https://ror.org/01qz5mb56".to_string())
.scheme("ROR".to_string())
.scheme_uri("https://ror.org".to_string())
.build();
let publisher = ActivityOrganization::init()
.name("Example Repository".to_string())
.identifiers(vec![publisher_identifier.clone()])
.roles(vec![DatedOrganizationRole::init()
.organization(publisher_identifier)
.role("Publisher".to_string())
.build()])
.build();
ResearchActivity {
meta: ResearchActivityMetadata {
identifier: "datacite-project".to_string(),
doi: Some(OneOrMany::Many(vec![
"https://doi.org/10.5072/acorn-project".to_string(),
"arXiv:2401.00001".to_string(),
])),
patents: Some(OneOrMany::One("US-1234567".to_string())),
typed_dates: Some(OneOrMany::One(
TypedActivityDate::init()
.date_type(ActivityDateType::Issued)
.date("2026-09-17T12:30:00-04:00".to_string())
.build(),
)),
organizations: Some(OneOrMany::One(publisher)),
related_resources: Some(OneOrMany::One(rrid_related_resource())),
access: Some(StructuredRights::init().access(RightsAccess::Open).build()),
..ResearchActivityMetadata::default()
},
title: "DataCite Project activity".to_string(),
subtitle: Some("An explicit project deposit".to_string()),
contact: ContactPoint::init()
.given_name("Jane".to_string())
.family_name("Doe".to_string())
.identifier("https://orcid.org/0000-0002-1825-0097".to_string())
.organization("Example Repository".to_string())
.build(),
..ResearchActivity::default()
}
}
fn infrastructure_environment() -> ComputeEnvironment {
serde_json::from_str(include_str!("../../../../../../../tests/fixtures/schema/ocp/infrastructure.json"))
.expect("infrastructure fixture should deserialize")
}
fn raid_contact() -> ContactPoint {
ContactPoint::init()
.given_name("Jane".to_string())
.family_name("Doe".to_string())
.identifier("https://orcid.org/0000-0002-1825-0097".to_string())
.email("jane.doe@example.com".to_string())
.build()
}
fn raid_ready_activity() -> ResearchActivity {
let agent = AgentIdentifier::init()
.value("https://orcid.org/0000-0002-1825-0097".to_string())
.scheme("ORCID".to_string())
.scheme_uri("https://orcid.org/".to_string())
.build();
let contributor = ActivityContributor::init()
.name("Jane Doe".to_string())
.identifiers(vec![agent.clone()])
.roles(vec![DatedAgentRole::init().agent(agent).leader(true).build()])
.build();
ResearchActivity {
meta: ResearchActivityMetadata {
identifier: "raid-crosswalk".to_string(),
typed_dates: Some(OneOrMany::Many(vec![TypedActivityDate::init()
.date_type(ActivityDateType::StartDate)
.date("2026-09-17T12:30:00-04:00".to_string())
.build()])),
contributors: Some(OneOrMany::Many(vec![contributor])),
access: Some(StructuredRights::init().access(RightsAccess::Open).build()),
..ResearchActivityMetadata::default()
},
title: "RAiD crosswalk activity".to_string(),
contact: raid_contact(),
..ResearchActivity::default()
}
}
fn rrid_related_resource() -> TypedRelatedResource {
TypedRelatedResource {
identifier: TypedIdentifier {
value: "rrid : SCR_003512".to_string(),
scheme: Some("rrid".to_string()),
scheme_uri: Some("https://scicrunch.org/resolver".to_string()),
},
relation_type: "Cites".to_string(),
relation_type_information: Some("Experimental software input".to_string()),
resource_type_general: Some("Software".to_string()),
category: Some(RelatedResourceCategory::Related),
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 64,
failure_persistence: Some(Box::new(FileFailurePersistence::Direct("tests/proptest-regressions/standard/crosswalk.txt"))),
..ProptestConfig::default()
})]
#[test]
fn prop_datacite_dcat_roundtrip(title in "\\PC{0,64}", publisher in "\\PC{0,64}") {
let mut source = Fields::new();
source.insert("doi", FieldValue::String("10.5555/test".to_string()));
source.insert("title", FieldValue::String(title.clone()));
source.insert("publisher", FieldValue::String(publisher.clone()));
let mut dcat_fields = Fields::new();
prop_assert!(datacite_to_dcat().apply(&source, &mut dcat_fields).is_ok());
let mut back = Fields::new();
prop_assert!(dcat_to_datacite().apply(&dcat_fields, &mut back).is_ok());
prop_assert_eq!(back.get_string_opt("title"), Some(title));
prop_assert_eq!(back.get_string_opt("publisher"), Some(publisher));
}
#[test]
fn prop_dcat_datacite_roundtrip(title in "\\PC{0,64}", desc in "\\PC{0,128}") {
let mut source = Fields::new();
source.insert("identifier", FieldValue::StringVec(vec!["10.5555/test".to_string()]));
source.insert("title", FieldValue::StringVec(vec![title.clone()]));
source.insert("description", FieldValue::StringVec(vec![desc.clone()]));
let mut data = Fields::new();
prop_assert!(dcat_to_datacite().apply(&source, &mut data).is_ok());
let mut back = Fields::new();
prop_assert!(datacite_to_dcat().apply(&data, &mut back).is_ok());
prop_assert_eq!(back.get_string_vec_opt("title"), Some(vec![title.clone()]));
prop_assert_eq!(back.get_string_vec_opt("description"), Some(vec![desc.clone()]));
}
#[test]
fn prop_mappings_never_panic(
strings in prop::collection::vec("\\PC{0,32}", 0..10),
numbers in prop::collection::vec(any::<f64>(), 0..10),
) {
let mut source = Fields::new();
for (i, s) in strings.iter().enumerate() {
source.insert(format!("str-{i}"), FieldValue::String(s.clone()));
}
for (i, n) in numbers.iter().enumerate() {
source.insert(format!("num-{i}"), FieldValue::Number(*n));
}
for mapping_fn in &[
datacite_to_dcat as fn() -> FieldMapping,
dcat_to_datacite as fn() -> FieldMapping,
datacite_to_huwise as fn() -> FieldMapping,
huwise_to_datacite as fn() -> FieldMapping,
datacite_to_invenio as fn() -> FieldMapping,
invenio_to_datacite as fn() -> FieldMapping,
] {
let mapping = mapping_fn();
let mut target = Fields::new();
let _ = mapping.apply(&source, &mut target);
}
}
#[test]
fn prop_rule_identity_string_roundtrip(value in "\\PC{0,64}") {
let mut source = Fields::new();
source.insert("src", FieldValue::String(value.clone()));
let mut mid = Fields::new();
prop_assert!(FieldRule::new("src", "mid").apply(&source, &mut mid).is_ok());
let mut target = Fields::new();
prop_assert!(FieldRule::new("mid", "tgt").apply(&mid, &mut target).is_ok());
prop_assert_eq!(target.get_string("tgt").ok(), Some(value));
}
#[test]
fn prop_rule_identity_stringvec_roundtrip(values in prop::collection::vec("\\PC{0,32}", 0..8)) {
let mut source = Fields::new();
source.insert("src", FieldValue::StringVec(values.clone()));
let mut mid = Fields::new();
prop_assert!(FieldRule::new("src", "mid").apply(&source, &mut mid).is_ok());
let mut target = Fields::new();
prop_assert!(FieldRule::new("mid", "tgt").apply(&mid, &mut target).is_ok());
prop_assert_eq!(target.get_string_vec_opt("tgt"), Some(values));
}
#[test]
fn prop_year_transform_roundtrip(year in 1000i32..=9999) {
let mut source = Fields::new();
source.insert("y", FieldValue::Number(year as f64));
let mut mid = Fields::new();
let fwd = FieldRule::new("y", "d").with_transform(year_to_date);
prop_assert!(fwd.apply(&source, &mut mid).is_ok());
let mut target = Fields::new();
let rev = FieldRule::new("d", "y").with_transform(extract_year);
prop_assert!(rev.apply(&mid, &mut target).is_ok());
prop_assert_eq!(target.get_number("y").ok(), Some(year as f64));
}
}
#[test]
fn build_datacite_from_fields() {
let mut fields = Fields::new();
fields.insert("doi", FieldValue::String("10.5555/87654321".to_string()));
fields.insert("title", FieldValue::String("Built Record".to_string()));
fields.insert("publication-year", FieldValue::Number(2025.0));
let record = datacite::Record::build_from_fields(&fields);
assert!(record.is_ok());
let record = record.unwrap();
assert_eq!(record.attributes.doi, "10.5555/87654321");
assert_eq!(record.attributes.titles.as_ref().map(|t| &t[0].title), Some(&"Built Record".to_string()));
assert_eq!(record.attributes.publication_year, Some(2025));
}
#[test]
fn build_dcat_from_fields() {
let mut fields = Fields::new();
fields.insert("identifier", FieldValue::StringVec(vec!["http://example.org/dataset/1".to_string()]));
fields.insert("title", FieldValue::String("Built Dataset".to_string()));
fields.insert("issued", FieldValue::Date("2025-03-20".to_string()));
let dataset = dcat::Dataset::build_from_fields(&fields);
assert!(dataset.is_ok());
let dataset = dataset.unwrap();
assert_eq!(
dataset.identifier,
Some(OneOrMany::Many(vec!["http://example.org/dataset/1".to_string()]))
);
assert_eq!(dataset.title, Some(OneOrMany::Many(vec!["Built Dataset".to_string()])));
assert_eq!(dataset.issued, Some("2025-03-20".to_string()));
}
#[test]
fn build_huwise_from_fields() {
let mut fields = Fields::new();
fields.insert("identifier", FieldValue::String("huwise-built-001".to_string()));
fields.insert("title", FieldValue::String("Built HuWise Dataset".to_string()));
fields.insert("description", FieldValue::String("Reconstructed from FieldMap".to_string()));
fields.insert("creators", FieldValue::StringVec(vec!["Alice".to_string(), "Bob".to_string()]));
fields.insert("publication-year", FieldValue::Number(2025.0));
fields.insert("license", FieldValue::String("CC-BY-4.0".to_string()));
let dataset = huwise::Dataset::build_from_fields(&fields);
assert!(dataset.is_ok());
let dataset = dataset.unwrap();
assert_eq!(dataset.dataset_id, "huwise-built-001");
assert_eq!(
dataset.metas.datacite.as_ref().and_then(|d| d.title.clone()),
Some("Built HuWise Dataset".to_string())
);
assert_eq!(
dataset.metas.datacite.as_ref().and_then(|d| d.publication_year.clone()),
Some("2025".to_string())
);
}
#[test]
fn build_invenio_from_fields() {
let mut fields = Fields::new();
fields.insert("identifier", FieldValue::String("invenio-built-001".to_string()));
fields.insert("title", FieldValue::String("Built Invenio Record".to_string()));
fields.insert("publication-year", FieldValue::Number(2025.0));
let record = invenio::Record::build_from_fields(&fields);
assert!(record.is_ok());
let record = record.unwrap();
assert_eq!(record.id, Some("invenio-built-001".to_string()));
assert_eq!(
record.metadata.as_ref().and_then(|m| m.title.clone()),
Some("Built Invenio Record".to_string())
);
assert_eq!(
record.metadata.as_ref().and_then(|m| m.publication_date.clone()),
Some("2025-01-01".to_string())
);
}
#[test]
fn convert_missing_required_field_fails() {
let _record = datacite::Record {
id: "".to_string(),
kind: "dois".to_string(),
attributes: datacite::Attributes {
doi: "".to_string(),
event: None,
titles: None,
creators: None,
publisher: None,
publication_year: None,
resource_types: None,
url: None,
subjects: None,
contributors: None,
dates: None,
language: None,
alternate_identifiers: None,
related_identifiers: None,
related_items: None,
sizes: None,
formats: None,
version: None,
rights_list: None,
descriptions: None,
geo_locations: None,
funding_references: None,
schema_version: None,
},
};
let dataset = dcat::Dataset {
id: None,
jsonld_type: None,
title: Some(OneOrMany::Many(vec!["No identifier".to_string()])),
description: None,
identifier: None,
issued: None,
modified: None,
language: None,
publisher: None,
creator: None,
contact_point: None,
keywords: None,
themes: None,
license: None,
rights: None,
access_rights: None,
has_policy: None,
conforms_to: None,
landing_page: None,
relation: None,
type_: None,
version: None,
version_notes: None,
previous_version: None,
has_version: None,
has_current_version: None,
replaces: None,
status: None,
is_referenced_by: None,
has_part: None,
qualified_relation: None,
first: None,
last: None,
previous: None,
distribution: None,
frequency: None,
in_series: None,
spatial: None,
spatial_resolution_in_meters: None,
temporal: None,
temporal_resolution: None,
was_generated_by: None,
};
let mapping = dcat_to_datacite();
let result: Result<(datacite::Record, Vec<ConversionWarning>), CrosswalkError> = crate::standard::crosswalk::convert(&dataset, &mapping);
assert!(matches!(result, Err(CrosswalkError::MissingRequiredField(_))));
}
#[test]
fn convert_propagates_missing_as_warnings() {
let record = datacite::Record {
id: "10.5555/warn-test".to_string(),
kind: "dois".to_string(),
attributes: datacite::Attributes {
doi: "10.5555/warn-test".to_string(),
event: None,
titles: Some(vec![datacite::Title {
title: "Warn Test".to_string(),
title_type: None,
}]),
creators: None,
publisher: None,
publication_year: None,
resource_types: None,
url: None,
subjects: None,
contributors: None,
dates: None,
language: None,
alternate_identifiers: None,
related_identifiers: None,
related_items: None,
sizes: None,
formats: None,
version: None,
rights_list: None,
descriptions: None,
geo_locations: None,
funding_references: None,
schema_version: None,
},
};
let mapping = datacite_to_dcat();
let result: Result<(dcat::Dataset, Vec<ConversionWarning>), CrosswalkError> = crate::standard::crosswalk::convert(&record, &mapping);
assert!(result.is_ok());
let (_, warnings) = result.expect("convert should succeed");
assert!(!warnings.is_empty(), "lossy conversion should produce warnings, got empty");
assert!(warnings
.iter()
.any(|w| w.field == "description" || w.field == "language" || w.field == "creators"));
for w in &warnings {
assert_eq!(w.from, "datacite");
assert_eq!(w.to, "dcat");
assert_eq!(w.loss_type, FieldLossType::Omitted);
assert!(w.details.as_deref().is_some_and(|details| details.contains(&w.field)));
}
}
#[test]
fn extract_datacite_record_basic() {
let record = datacite::Record {
id: "10.5555/12345678".to_string(),
kind: "dois".to_string(),
attributes: datacite::Attributes {
doi: "10.5555/12345678".to_string(),
event: None,
titles: Some(vec![datacite::Title {
title: "Test Dataset".to_string(),
title_type: None,
}]),
creators: None,
publisher: None,
publication_year: Some(2024),
resource_types: None,
url: None,
subjects: None,
contributors: None,
dates: None,
language: Some("en".to_string()),
alternate_identifiers: None,
related_identifiers: None,
related_items: None,
sizes: None,
formats: None,
version: None,
rights_list: None,
descriptions: None,
geo_locations: None,
funding_references: None,
schema_version: None,
},
};
let fields = record.extract_fields();
assert_eq!(fields.get_string("doi").ok(), Some("10.5555/12345678".to_string()));
assert_eq!(fields.get_string("title").ok(), Some("Test Dataset".to_string()));
assert_eq!(fields.get_number("publication-year").ok(), Some(2024.0));
}
#[test]
fn extract_dcat_dataset_basic() {
let dataset = dcat::Dataset {
id: None,
jsonld_type: None,
title: Some(OneOrMany::Many(vec!["Test Dataset".to_string()])),
description: Some(OneOrMany::Many(vec!["A test dataset".to_string()])),
identifier: Some(OneOrMany::Many(vec!["10.5555/12345678".to_string()])),
issued: Some("2024-01-15".to_string()),
modified: None,
language: Some(OneOrMany::Many(vec!["en".to_string()])),
publisher: None,
creator: None,
contact_point: None,
keywords: Some(OneOrMany::Many(vec!["test".to_string(), "data".to_string()])),
themes: None,
license: None,
rights: None,
access_rights: None,
has_policy: None,
conforms_to: None,
landing_page: None,
relation: None,
type_: None,
version: None,
version_notes: None,
previous_version: None,
has_version: None,
has_current_version: None,
replaces: None,
status: None,
is_referenced_by: None,
has_part: None,
qualified_relation: None,
first: None,
last: None,
previous: None,
distribution: None,
frequency: None,
in_series: None,
spatial: None,
spatial_resolution_in_meters: None,
temporal: None,
temporal_resolution: None,
was_generated_by: None,
};
let fields = dataset.extract_fields();
assert_eq!(fields.get_string_vec("identifier").ok(), Some(vec!["10.5555/12345678".to_string()]));
assert_eq!(fields.get_string("title").ok(), Some("Test Dataset".to_string()));
assert_eq!(fields.get_string_vec_opt("keywords"), Some(vec!["test".to_string(), "data".to_string()]));
}
#[test]
fn extract_huwise_dataset_with_datacite_block() {
let dataset = huwise::Dataset {
dataset_id: "huwise-001".to_string(),
has_attachments: false,
attachments_count: 0,
has_records: false,
fields: json!([]),
metas: huwise::Meta {
datacite: Some(huwise::Datacite {
identifier: Some("10.5555/huwise-test".to_string()),
title: Some("HuWise DataCite Block Test".to_string()),
alternative_title: None,
publisher: None,
creator: Some(json!(["Jane Smith", "John Doe"])),
publication_year: Some("2024".to_string()),
subject: Some(vec!["data".to_string(), "science".to_string()]),
contributor: None,
date: None,
language: Some("en".to_string()),
resource_type: Some(json!("Dataset")),
alternate_identifier: None,
related_identifier: None,
size: None,
format: None,
version: Some("1.0".to_string()),
rights: Some(json!("CC-BY-4.0")),
description: Some("Test dataset with DataCite metadata".to_string()),
geolocation: None,
}),
r#default: None,
dublin_core: None,
dcat: None,
dcat_ap: None,
custom_template: None,
},
features: vec![],
};
let fields = dataset.extract_fields();
assert_eq!(fields.get_string("identifier").ok(), Some("huwise-001".to_string()));
assert_eq!(fields.get_string("title").ok(), Some("HuWise DataCite Block Test".to_string()));
assert_eq!(fields.get_number("publication-year").ok(), Some(2024.0));
assert_eq!(
fields.get_string_vec("creators").ok(),
Some(vec!["Jane Smith".to_string(), "John Doe".to_string()])
);
}
#[test]
fn extract_huwise_dataset_with_default_block() {
let dataset = huwise::Dataset {
dataset_id: "huwise-default-001".to_string(),
has_attachments: false,
attachments_count: 0,
has_records: false,
fields: json!([]),
metas: huwise::Meta {
datacite: None,
r#default: Some(huwise::DefaultMeta {
title: Some("HuWise Default Block Test".to_string()),
description: Some("A comprehensive test dataset".to_string()),
keyword: Some(vec!["test".to_string(), "default".to_string()]),
language: Some("en".to_string()),
publisher: Some("Test Publisher".to_string()),
license: Some("CC-BY-4.0".to_string()),
modified: Some("2024-01-15T10:00:00Z".to_string()),
theme: None,
metadata_languages: None,
timezone: None,
modified_updates_on_metadata_change: None,
modified_updates_on_data_change: None,
data_processed: None,
metadata_processed: None,
geographic_reference: None,
geographic_reference_auto: None,
territory: None,
geometry_types: None,
bbox: None,
references: None,
records_count: None,
attributions: None,
source_domain: None,
source_domain_title: None,
source_domain_address: None,
source_dataset: None,
shared_catalog: None,
federated: None,
parent_domain: None,
update_frequency: None,
license_url: None,
}),
dublin_core: None,
dcat: None,
dcat_ap: None,
custom_template: None,
},
features: vec![],
};
let fields = dataset.extract_fields();
assert_eq!(fields.get_string("title").ok(), Some("HuWise Default Block Test".to_string()));
assert_eq!(fields.get_string("description").ok(), Some("A comprehensive test dataset".to_string()));
assert_eq!(
fields.get_string_vec("subjects").ok(),
Some(vec!["test".to_string(), "default".to_string()])
);
}
#[test]
fn extract_invenio_record_basic() {
let record = invenio::Record {
schema: Some("https://inveniordm.docs.cern.ch/reference/metadata/".to_string()),
id: Some("test-invenio-001".to_string()),
pid: Some(invenio::PersistentIdentifier {
pk: Some(1),
status: Some("R".to_string()),
}),
pids: None,
parent: None,
access: None,
metadata: Some(invenio::Metadata {
resource_type: None,
title: Some("Invenio Test Dataset".to_string()),
publication_date: Some("2024-06-15".to_string()),
creators: None,
additional_titles: None,
description: None,
additional_descriptions: None,
rights: None,
copyright: None,
contributors: None,
subjects: None,
languages: None,
dates: None,
version: None,
publisher: None,
identifiers: None,
related_identifiers: None,
sizes: None,
formats: None,
locations: None,
funding: None,
references: None,
}),
custom_fields: None,
files: None,
tombstone: None,
created: Some("2024-06-15T10:00:00Z".to_string()),
updated: Some("2024-06-16T12:00:00Z".to_string()),
};
let fields = record.extract_fields();
assert_eq!(fields.get_string("identifier").ok(), Some("1".to_string()));
assert_eq!(fields.get_string("title").ok(), Some("Invenio Test Dataset".to_string()));
assert_eq!(fields.get_number("publication-year").ok(), Some(2024.0));
}
#[test]
fn from_datacite_to_dcat() {
let record = datacite::Record {
id: "10.5555/12345678".to_string(),
kind: "dois".to_string(),
attributes: datacite::Attributes {
doi: "10.5555/12345678".to_string(),
event: None,
titles: Some(vec![datacite::Title {
title: "Science Dataset".to_string(),
title_type: None,
}]),
creators: None,
publisher: None,
publication_year: Some(2024),
resource_types: None,
url: Some("https://example.org/dataset".to_string()),
subjects: None,
contributors: None,
dates: None,
language: Some("en".to_string()),
alternate_identifiers: None,
related_identifiers: None,
related_items: None,
sizes: None,
formats: None,
version: None,
rights_list: None,
descriptions: Some(vec![datacite::Description {
description: "A scientific dataset".to_string(),
description_type: None,
language: None,
}]),
geo_locations: None,
funding_references: None,
schema_version: None,
},
};
let dataset: dcat::Dataset = record.try_into().unwrap();
assert_eq!(dataset.title, Some(OneOrMany::Many(vec!["Science Dataset".to_string()])));
assert_eq!(dataset.description, Some(OneOrMany::Many(vec!["A scientific dataset".to_string()])));
assert_eq!(dataset.language, Some(OneOrMany::Many(vec!["en".to_string()])));
assert_eq!(dataset.jsonld_type.as_deref(), Some("dcat:Dataset"));
assert!(matches!(
dataset.landing_page.as_ref().and_then(|values| values.first()),
Some(dcat::DocumentRef::Uri(_))
));
}
#[test]
fn from_datacite_to_invenio() {
let record = datacite::Record {
id: "10.5555/datacite-to-inv".to_string(),
kind: "dois".to_string(),
attributes: datacite::Attributes {
doi: "10.5555/datacite-to-inv".to_string(),
event: None,
titles: Some(vec![datacite::Title {
title: "DataCite to Invenio Test".to_string(),
title_type: None,
}]),
creators: None,
publisher: None,
publication_year: Some(2024),
resource_types: None,
url: None,
subjects: None,
contributors: None,
dates: None,
language: Some("de".to_string()),
alternate_identifiers: None,
related_identifiers: None,
related_items: None,
sizes: None,
formats: None,
version: Some("1.0".to_string()),
rights_list: None,
descriptions: Some(vec![datacite::Description {
description: "A datacite test record".to_string(),
description_type: None,
language: None,
}]),
geo_locations: None,
funding_references: None,
schema_version: None,
},
};
let invenio_record: invenio::Record = record.try_into().unwrap();
assert_eq!(invenio_record.id, Some("10.5555/datacite-to-inv".to_string()));
assert_eq!(
invenio_record.metadata.as_ref().and_then(|m| m.title.clone()),
Some("DataCite to Invenio Test".to_string())
);
assert_eq!(invenio_record.metadata.as_ref().and_then(|m| m.version.clone()), Some("1.0".to_string()));
assert_eq!(
invenio_record.metadata.as_ref().and_then(|m| m.languages.as_ref()).map(|l| &l[0].id),
Some(&"de".to_string())
);
}
#[test]
fn from_dcat_to_datacite() {
let dataset = dcat::Dataset {
id: None,
jsonld_type: None,
title: Some(OneOrMany::Many(vec!["Open Data Collection".to_string()])),
description: Some(OneOrMany::Many(vec!["Public data for research".to_string()])),
identifier: Some(OneOrMany::Many(vec!["https://doi.org/10.5555/87654321".to_string()])),
issued: Some("2024-06-15".to_string()),
modified: None,
language: Some(OneOrMany::Many(vec!["fr".to_string()])),
publisher: None,
creator: None,
contact_point: None,
keywords: Some(OneOrMany::Many(vec!["open".to_string(), "data".to_string()])),
themes: None,
license: Some("https://creativecommons.org/licenses/by/4.0/".to_string()),
rights: None,
access_rights: None,
has_policy: None,
conforms_to: None,
landing_page: None,
relation: None,
type_: None,
version: None,
version_notes: None,
previous_version: None,
has_version: None,
has_current_version: None,
replaces: None,
status: None,
is_referenced_by: None,
has_part: None,
qualified_relation: None,
first: None,
last: None,
previous: None,
distribution: None,
frequency: None,
in_series: None,
spatial: None,
spatial_resolution_in_meters: None,
temporal: None,
temporal_resolution: None,
was_generated_by: None,
};
let record: datacite::Record = dataset.try_into().unwrap();
assert_eq!(record.attributes.doi, "https://doi.org/10.5555/87654321");
assert_eq!(
record.attributes.titles.as_ref().map(|t| &t[0].title),
Some(&"Open Data Collection".to_string())
);
assert_eq!(
record.attributes.descriptions.as_ref().map(|d| &d[0].description),
Some(&"Public data for research".to_string())
);
assert_eq!(record.attributes.language, Some("fr".to_string()));
}
#[test]
fn from_huwise_to_datacite() {
let dataset = huwise::Dataset {
dataset_id: "huwise-to-dc-001".to_string(),
has_attachments: false,
attachments_count: 0,
has_records: false,
fields: json!([]),
metas: huwise::Meta {
datacite: Some(huwise::Datacite {
identifier: Some("10.5555/huwise-convert".to_string()),
title: Some("HuWise to DataCite Test".to_string()),
alternative_title: None,
publisher: Some("Publisher".to_string()),
creator: Some(json!(["Test Author"])),
publication_year: Some("2024".to_string()),
subject: None,
contributor: None,
date: None,
description: Some("Test conversion".to_string()),
language: Some("en".to_string()),
resource_type: Some(json!("Dataset")),
alternate_identifier: None,
related_identifier: None,
size: None,
format: None,
version: None,
rights: Some(json!("CC-BY-4.0")),
geolocation: None,
}),
r#default: None,
dublin_core: None,
dcat: None,
dcat_ap: None,
custom_template: None,
},
features: vec![],
};
let datacite_record: datacite::Record = dataset.try_into().unwrap();
assert_eq!(
datacite_record.attributes.titles.as_ref().map(|t| &t[0].title),
Some(&"HuWise to DataCite Test".to_string())
);
assert_eq!(
datacite_record.attributes.descriptions.as_ref().map(|d| &d[0].description),
Some(&"Test conversion".to_string())
);
assert_eq!(datacite_record.attributes.language, Some("en".to_string()));
}
#[test]
fn from_invenio_to_datacite() {
let record = invenio::Record {
schema: Some("https://inveniordm.docs.cern.ch/reference/metadata/".to_string()),
id: Some("invenio-convert-001".to_string()),
pid: Some(invenio::PersistentIdentifier {
pk: None,
status: Some("U".to_string()),
}),
pids: None,
parent: None,
access: None,
metadata: Some(invenio::Metadata {
resource_type: None,
title: Some("Invenio to DataCite Test".to_string()),
publication_date: Some("2024-03-01".to_string()),
creators: None,
additional_titles: None,
description: Some("A test record".to_string()),
additional_descriptions: None,
rights: None,
copyright: None,
contributors: None,
subjects: None,
languages: Some(vec![invenio::Language { id: "en".to_string() }]),
dates: None,
version: None,
publisher: None,
identifiers: None,
related_identifiers: None,
sizes: None,
formats: None,
locations: None,
funding: None,
references: None,
}),
custom_fields: None,
files: None,
tombstone: None,
created: Some("2024-03-01T00:00:00Z".to_string()),
updated: None,
};
let datacite_record: datacite::Record = record.try_into().unwrap();
assert_eq!(datacite_record.attributes.doi, "invenio-convert-001");
assert_eq!(
datacite_record.attributes.titles.as_ref().map(|t| &t[0].title),
Some(&"Invenio to DataCite Test".to_string())
);
assert_eq!(
datacite_record.attributes.descriptions.as_ref().map(|d| &d[0].description),
Some(&"A test record".to_string())
);
assert_eq!(datacite_record.attributes.language, Some("en".to_string()));
}
#[test]
fn mapping_apply_basic() {
let mut source = Fields::new();
source.insert("title", FieldValue::String("Test Title".to_string()));
let mut target = Fields::new();
let rule = FieldRule::new("title", "name");
rule.apply(&source, &mut target).ok();
assert_eq!(target.get_string_opt("name"), Some("Test Title".to_string()));
}
#[test]
fn mapping_missing_optional_field() {
let source = Fields::new();
let mut target = Fields::new();
let rule = FieldRule::new("title", "name");
let missing = rule.apply(&source, &mut target).ok().flatten();
assert_eq!(missing, Some("title".to_string()));
assert!(target.is_empty());
}
#[test]
fn mapping_required_field_fails() {
let source = Fields::new();
let mut target = Fields::new();
let rule = FieldRule::new("title", "name").required();
let err = rule.apply(&source, &mut target);
assert!(err.is_err());
match err {
| Err(CrosswalkError::MissingRequiredField(f)) => {
assert_eq!(f, "title");
}
| _ => panic!("expected MissingRequiredField error"),
}
}
#[test]
fn test_datacite_contributor_type_infers_research_activity_roles() {
assert_eq!(
datacite::ContributorType::try_from("Lead Research Organisation").unwrap(),
datacite::ContributorType::HostingInstitution
);
assert_eq!(datacite::ContributorType::try_from("Funder").unwrap(), datacite::ContributorType::Sponsor);
assert!(datacite::ContributorType::try_from("Unsupported role").is_err());
}
#[test]
fn test_raid_research_activity_crosswalk_imports_pinned_mint_fixture() {
let metadata: raid::Metadata = serde_json::from_str(include_str!("../../../../../../../tests/fixtures/raid/mint.json")).unwrap();
let contact = ContactPoint::init()
.given_name("Fixture".to_string())
.family_name("Contact".to_string())
.build();
let options = RaidImportOptions {
contact: Some(contact),
identifier: Some("fixture-raid".to_string()),
contributor_names: BTreeMap::new(),
organization_names: BTreeMap::new(),
};
let (activity, warnings) = metadata.to_research_activity(&options).unwrap();
assert!(activity.validate().is_ok());
assert_eq!(activity.meta.identifier, "fixture-raid");
assert_eq!(activity.title, "Created from API");
assert!(activity.contact.identifier.is_none());
assert!(warnings.iter().any(|warning| warning.field == "contributor[contact].id"));
assert!(warnings.iter().any(|warning| warning.field == "date.startDate"));
}
#[test]
fn test_raid_research_activity_crosswalk_rejects_ambiguous_or_conflicting_identity() {
let (metadata, _) = raid_ready_activity().to_raid(&RaidExportOptions::default()).unwrap();
let mut conflicting_contact = raid_contact();
conflicting_contact.identifier = Some("https://orcid.org/0000-0002-2057-9115".to_string());
let options = RaidImportOptions {
contact: Some(conflicting_contact),
identifier: None,
contributor_names: BTreeMap::new(),
organization_names: BTreeMap::new(),
};
assert!(matches!(
metadata.to_research_activity(&options),
Err(CrosswalkError::TransformationFailed { field, .. }) if field == "options.contact.identifier"
));
let mut ambiguous_title = metadata.clone();
let primary = ambiguous_title.title.as_ref().unwrap()[0].clone();
ambiguous_title.title.as_mut().unwrap().push(primary);
let options = RaidImportOptions {
contact: Some(raid_contact()),
identifier: None,
contributor_names: BTreeMap::new(),
organization_names: BTreeMap::new(),
};
assert!(matches!(
ambiguous_title.to_research_activity(&options),
Err(CrosswalkError::TransformationFailed { field, .. }) if field == "title"
));
let mut ambiguous_identifier = metadata;
ambiguous_identifier
.alternate_identifier
.as_mut()
.unwrap()
.push(raid::AlternateIdentifier {
id: "second-local-identifier".to_string(),
alternate_identifier_type: "ACORN".to_string(),
});
assert!(matches!(
ambiguous_identifier.to_research_activity(&options),
Err(CrosswalkError::TransformationFailed { field, .. }) if field == "alternateIdentifier[ACORN]"
));
}
#[test]
fn test_raid_research_activity_crosswalk_requires_local_identity_context() {
let (metadata, _) = raid_ready_activity().to_raid(&RaidExportOptions::default()).unwrap();
assert!(matches!(
<raid::Metadata as Crosswalk<ResearchActivity>>::crosswalk(&metadata),
Err(CrosswalkError::MissingRequiredField(field)) if field == "options.contact"
));
let options = RaidImportOptions {
contact: Some(raid_contact()),
identifier: None,
contributor_names: BTreeMap::new(),
organization_names: BTreeMap::new(),
};
let (activity, warnings) = metadata.to_research_activity(&options).unwrap();
assert!(activity.validate().is_ok());
assert_eq!(activity.meta.identifier, "raid-crosswalk");
assert_eq!(activity.title, "RAiD crosswalk activity");
assert_eq!(activity.contact.identifier.as_deref(), Some("https://orcid.org/0000-0002-1825-0097"));
assert!(activity
.meta
.typed_dates
.as_ref()
.unwrap()
.iter()
.any(|date| date.date == "2026-09-17T00:00:00Z"));
assert!(warnings.iter().any(|warning| warning.field == "date.startDate"));
}
#[test]
fn test_raid_research_activity_crosswalk_uses_name_maps_and_retains_unmapped_rors() {
let mut metadata = raid_ready_activity().to_raid(&RaidExportOptions::default()).unwrap().0;
metadata.organization = Some(vec![raid::Organization {
id: "https://ror.org/01qz5mb56".to_string(),
schema_uri: Some("https://ror.org/".to_string()),
role: vec![raid::OrganizationRole {
id: raid::OrganizationRoleType::LeadResearchOrganization,
schema_uri: None,
date: crate::Date {
start_date: Some("2026-09-17".to_string()),
end_date: None,
},
}],
}]);
let mut organization_names = BTreeMap::new();
organization_names.insert("01qz5mb56".to_string(), "Oak Ridge National Laboratory".to_string());
let options = RaidImportOptions {
contact: Some(raid_contact()),
identifier: None,
contributor_names: BTreeMap::new(),
organization_names,
};
let (activity, warnings) = metadata.to_research_activity(&options).unwrap();
assert_eq!(
activity.meta.organizations.as_ref().unwrap().first().unwrap().name,
"Oak Ridge National Laboratory"
);
assert!(activity.meta.ror.is_none());
assert!(!warnings.iter().any(|warning| warning.field == "organization[]"));
let unmapped = RaidImportOptions {
organization_names: BTreeMap::new(),
..options
};
let (activity, warnings) = metadata.to_research_activity(&unmapped).unwrap();
assert_eq!(
activity.meta.ror.as_ref().and_then(OneOrMany::first).map(String::as_str),
Some("https://ror.org/01qz5mb56")
);
assert!(warnings.iter().any(|warning| warning.field == "organization[]"));
}
#[test]
fn test_research_activity_datacite_project_crosswalk_is_deposit_ready_and_warning_aware() {
let activity = datacite_project_activity();
let (record, warnings) = activity.to_datacite_project().unwrap();
assert!(record.validate_deposit_ready().is_ok());
assert_eq!(record.id, "10.5072/acorn-project");
assert_eq!(
record
.attributes
.resource_types
.as_ref()
.and_then(|types| types.resource_type_general.as_ref()),
Some(&datacite::ResourceTypeGeneral::Project)
);
assert_eq!(record.attributes.publisher.as_ref().unwrap().name, "Example Repository");
assert_eq!(
record.attributes.publisher.as_ref().unwrap().publisher_identifier.as_deref(),
Some("https://ror.org/01qz5mb56")
);
assert_eq!(
record.attributes.titles.as_ref().unwrap()[1].title_type,
Some(datacite::TitleType::Subtitle)
);
let rrid = record
.attributes
.related_identifiers
.as_ref()
.unwrap()
.iter()
.find(|related| related.related_identifier_type == Some(datacite::RelatedIdentifierType::Rrid))
.unwrap();
assert_eq!(rrid.relation_type, Some(datacite::RelationType::Cites));
assert!(record
.attributes
.related_identifiers
.as_ref()
.unwrap()
.iter()
.any(|related| related.related_identifier_type == Some(datacite::RelatedIdentifierType::Arxiv)));
assert!(record.attributes.rights_list.is_none());
assert!(warnings.iter().any(|warning| warning.field == "meta.access.access"));
assert!(warnings.iter().any(|warning| warning.field == "meta.patents[0]"));
assert!(warnings.iter().any(|warning| warning.field == "meta.status"));
assert!(warnings.iter().all(|warning| warning.from == "rad" && warning.to == "datacite"));
}
#[test]
fn test_research_activity_datacite_project_crosswalk_rejects_missing_or_ambiguous_deposit_assertions() {
let mut activity = datacite_project_activity();
activity.meta.doi = None;
assert!(matches!(
activity.to_datacite_project(),
Err(CrosswalkError::MissingRequiredField(field)) if field == "meta.doi[project]"
));
activity = datacite_project_activity();
activity.meta.typed_dates = None;
assert!(matches!(
activity.to_datacite_project(),
Err(CrosswalkError::MissingRequiredField(field)) if field == "meta.typedDates[Issued]"
));
activity = datacite_project_activity();
activity.meta.organizations = None;
assert!(matches!(
activity.to_datacite_project(),
Err(CrosswalkError::MissingRequiredField(field)) if field == "meta.organizations[role=Publisher]"
));
activity = datacite_project_activity();
activity.meta.doi = Some(OneOrMany::Many(vec!["10.5072/one".to_string(), "10.5072/two".to_string()]));
assert!(matches!(
activity.to_datacite_project(),
Err(CrosswalkError::TransformationFailed { field, .. }) if field == "meta.doi"
));
}
#[test]
fn test_research_activity_infrastructure_is_reported_as_local_only() {
let environment = infrastructure_environment();
let mut datacite = datacite_project_activity();
datacite.meta.infrastructure = Some(OneOrMany::One(environment.clone()));
let (_, warnings) = datacite.to_datacite_project().expect("DataCite project conversion should succeed");
assert!(warnings.iter().any(|warning| {
warning.field == "meta.infrastructure"
&& warning.loss_type == FieldLossType::NoEquivalent
&& warning.from == "rad"
&& warning.to == "datacite"
}));
let mut raid = raid_ready_activity();
raid.meta.infrastructure = Some(OneOrMany::One(environment));
let (_, warnings) = raid.to_raid(&RaidExportOptions::default()).expect("RAiD conversion should succeed");
assert!(warnings.iter().any(|warning| {
warning.field == "meta.infrastructure" && warning.loss_type == FieldLossType::NoEquivalent && warning.from == "rad" && warning.to == "raid"
}));
}
#[test]
fn test_research_activity_legacy_and_canonical_field_conflicts_are_rejected() {
let mut sponsors = serde_json::to_value(ResearchActivity::default()).unwrap();
sponsors["meta"]["funders"] = json!(["Canonical Funder"]);
sponsors["meta"]["sponsors"] = json!(["Legacy Funder"]);
let error = serde_json::from_value::<ResearchActivity>(sponsors).unwrap_err();
assert!(error.to_string().contains("duplicate field `funders`"));
let mut related = serde_json::to_value(ResearchActivity::default()).unwrap();
related["meta"]["relatedResources"] = json!(["canonical-activity"]);
related["meta"]["related"] = json!(["legacy-activity"]);
let error = serde_json::from_value::<ResearchActivity>(related).unwrap_err();
assert!(error.to_string().contains("duplicate field `relatedResources`"));
}
#[test]
fn test_research_activity_raid_crosswalk_inference_is_explicit() {
let activity = ResearchActivity {
meta: ResearchActivityMetadata {
identifier: "legacy-raid-crosswalk".to_string(),
raid: Some(OneOrMany::One("10.83962/fb5be317".to_string())),
ror: Some(OneOrMany::One("https://ror.org/01qz5mb56".to_string())),
funders: Some(OneOrMany::One(
Funding::init()
.name("Oak Ridge National Laboratory".to_string())
.ror("https://ror.org/01qz5mb56".to_string())
.build(),
)),
..ResearchActivityMetadata::default()
},
title: "Legacy RAiD crosswalk".to_string(),
contact: raid_contact(),
..ResearchActivity::default()
};
assert!(activity.to_raid(&RaidExportOptions::default()).is_err());
let options = RaidExportOptions {
allow_infer: true,
inference_date: Some("2026-09-17".to_string()),
identifier: None,
};
let (metadata, warnings) = activity.to_raid(&options).unwrap();
assert!(metadata.validate().is_ok());
assert_eq!(metadata.date.as_ref().and_then(|date| date.start_date.as_deref()), Some("2026-09-17"));
assert_eq!(metadata.identifier.as_ref().and_then(|identifier| identifier.version), Some(1));
let organization = metadata.organization.as_ref().unwrap().first().unwrap();
assert!(organization.role.iter().any(|role| role.id.is_funder()));
assert!(organization.role.iter().any(|role| role.id.is_lead_research_organization()));
assert!(warnings
.iter()
.any(|warning| { warning.field == "date.startDate" && warning.loss_type == FieldLossType::Approximated }));
assert!(warnings
.iter()
.any(|warning| { warning.field == "identifier.license" && warning.loss_type == FieldLossType::Approximated }));
let missing_date = RaidExportOptions {
inference_date: None,
..options
};
assert!(matches!(
activity.to_raid(&missing_date),
Err(CrosswalkError::MissingRequiredField(field)) if field == "options.inferenceDate"
));
let fully_authored = raid_ready_activity();
assert!(matches!(
fully_authored.to_raid(&RaidExportOptions {
allow_infer: true,
..RaidExportOptions::default()
}),
Err(CrosswalkError::MissingRequiredField(field)) if field == "options.inferenceDate"
));
}
#[test]
fn test_research_activity_raid_crosswalk_is_strict_and_warning_aware() {
let activity = raid_ready_activity();
let (metadata, warnings) = activity.to_raid(&RaidExportOptions::default()).unwrap();
assert!(metadata.validate().is_ok());
assert_eq!(metadata.date.as_ref().and_then(|date| date.start_date.as_deref()), Some("2026-09-17"));
assert_eq!(metadata.title.as_ref().unwrap()[0].text.as_deref(), Some("RAiD crosswalk activity"));
assert!(metadata
.contributor
.as_ref()
.unwrap()
.iter()
.any(|contributor| contributor.contact && contributor.leader));
assert!(warnings.iter().any(|warning| warning.field == "typedDates[StartDate]"));
assert!(warnings.iter().any(|warning| warning.field == "contributors[].name"));
let mut missing_date = raid_ready_activity();
missing_date.meta.typed_dates = None;
assert!(matches!(
missing_date.to_raid(&RaidExportOptions::default()),
Err(CrosswalkError::MissingRequiredField(field)) if field == "typedDates[StartDate]"
));
let mut malformed_date = raid_ready_activity();
malformed_date.meta.typed_dates = Some(OneOrMany::One(
TypedActivityDate::init()
.date_type(ActivityDateType::StartDate)
.date("2026-09-17Tinvalid".to_string())
.build(),
));
assert!(matches!(
malformed_date.to_raid(&RaidExportOptions::default()),
Err(CrosswalkError::MissingRequiredField(field)) if field == "typedDates[StartDate]"
));
let mut missing_access = raid_ready_activity();
missing_access.meta.access = None;
assert!(matches!(
missing_access.to_raid(&RaidExportOptions::default()),
Err(CrosswalkError::MissingRequiredField(field)) if field == "meta.access"
));
let mut missing_leader = raid_ready_activity();
missing_leader.meta.contributors = None;
assert!(matches!(
missing_leader.to_raid(&RaidExportOptions::default()),
Err(CrosswalkError::MissingRequiredField(field)) if field == "contributors[].leader"
));
let mut ambiguous_leader = raid_ready_activity();
let contributor = ambiguous_leader.meta.contributors.as_ref().unwrap().first().unwrap().clone();
ambiguous_leader.meta.contributors = Some(OneOrMany::Many(vec![contributor.clone(), contributor]));
assert!(matches!(
ambiguous_leader.to_raid(&RaidExportOptions::default()),
Err(CrosswalkError::TransformationFailed { field, .. }) if field == "contributors[].leader"
));
}
#[test]
fn test_research_output_crosswalk_rejects_incomplete_deposit_metadata() {
let output = ResearchOutput::init().title("Incomplete output").build();
let result = <ResearchOutput as Crosswalk<datacite::Record>>::crosswalk(&output);
assert!(matches!(result, Err(CrosswalkError::MissingRequiredField(fields)) if fields.contains("doi") && fields.contains("creators")));
}
#[test]
fn test_research_output_datacite_round_trip_preserves_supported_fields_and_reports_credit_loss() {
let output = ResearchOutput::init()
.identifier("provider:record-1")
.doi("10.5072/acorn-output")
.title("Standards-aligned output")
.publication_year(2026)
.dates(vec![TypedActivityDate::init()
.date_type(ActivityDateType::Accepted)
.date("2026-09-01T00:00:00Z")
.build()])
.publisher(datacite::Publisher {
name: "Example Repository".to_string(),
publisher_identifier: Some("https://ror.org/01qz5mb56".to_string()),
publisher_identifier_scheme: Some("ROR".to_string()),
scheme_uri: Some("https://ror.org".to_string()),
})
.resource_type_general(datacite::ResourceTypeGeneral::Dataset)
.resource_type("Benchmark dataset")
.language("en")
.alternate_identifiers(vec![TypedIdentifier::init()
.value("swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2")
.scheme("SWHID")
.build()])
.related_identifiers(vec![
TypedRelatedResource::init()
.identifier(TypedIdentifier::init().value("10.26259/example").scheme("RAiD").build())
.relation_type("Other")
.relation_type_information("Produced by the registered activity")
.resource_type_general("Project")
.build(),
TypedRelatedResource::init()
.identifier(TypedIdentifier::init().value("RRID:SCR_003512").scheme("RRID").build())
.relation_type("Cites")
.resource_type_general("Software")
.build(),
TypedRelatedResource::init()
.identifier(
TypedIdentifier::init()
.value("swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2")
.scheme("SWHID")
.build(),
)
.relation_type("IsVersionOf")
.resource_type_general("Software")
.build(),
])
.rights_list(vec![StructuredRights::init()
.rights_identifier("CC-BY-4.0")
.rights_uri("https://creativecommons.org/licenses/by/4.0/")
.rights_identifier_scheme("SPDX")
.build()])
.subjects(vec![ControlledSubject::init()
.subject("Climate science")
.subject_scheme("Example Subjects")
.scheme_uri("https://example.org/subjects")
.value_uri("https://example.org/subjects/climate-science")
.build()])
.formats(vec!["application/json".to_string()])
.sizes(vec!["6 MB".to_string()])
.creators(vec![Contributor::init()
.name("Example Laboratory")
.name_type(datacite::NameType::Organizational)
.ror("https://ror.org/01qz5mb56")
.build()])
.contributors(vec![Contributor::init()
.name("Doe, Jane")
.name_type(datacite::NameType::Personal)
.orcid("0000-0002-1825-0097")
.contributor_type(datacite::ContributorType::DataCurator)
.roles(vec![CreditRole::DataCuration])
.build()])
.funding(vec![Funding::init()
.name("Example Funder")
.identifier("501100000000")
.identifier_type(datacite::FunderIdentifierType::CrossrefFunderId)
.scheme_uri("https://doi.org/10.13039")
.awards(vec![Award::init()
.identifier("AWARD-1")
.doi("10.5072/example-award")
.title("Example Award")
.build()])
.build()])
.keywords(vec!["open-science".to_string()])
.build();
assert!(output.validate().is_ok());
assert!(output.validate_deposit_ready().is_ok());
let (record, warnings): (datacite::Record, _) = output.crosswalk().unwrap();
assert!(record.validate_deposit_ready().is_ok());
assert_eq!(
record.attributes.related_identifiers.as_ref().unwrap()[0].related_identifier_type,
Some(datacite::RelatedIdentifierType::Raid)
);
assert_eq!(
record.attributes.related_identifiers.as_ref().unwrap()[0].relation_type,
Some(datacite::RelationType::Other)
);
assert_eq!(
record.attributes.related_identifiers.as_ref().unwrap()[1].related_identifier_type,
Some(datacite::RelatedIdentifierType::Rrid)
);
assert_eq!(
record.attributes.related_identifiers.as_ref().unwrap()[2].related_identifier_type,
Some(datacite::RelatedIdentifierType::Swhid)
);
assert_eq!(
record.attributes.creators.as_ref().unwrap()[0].name_type,
Some(datacite::NameType::Organizational)
);
assert_eq!(
record.attributes.contributors.as_ref().unwrap()[0].contributor_type,
datacite::ContributorType::DataCurator
);
assert_eq!(
warnings.iter().map(|warning| warning.field.as_str()).collect::<Vec<_>>(),
vec!["contributors[].roles"]
);
let (round_trip, reverse_warnings): (ResearchOutput, _) = record.crosswalk().unwrap();
assert!(reverse_warnings.is_empty());
assert_eq!(round_trip.identifier, output.identifier);
assert_eq!(round_trip.doi, output.doi);
assert_eq!(round_trip.title, output.title);
assert_eq!(round_trip.publication_year, output.publication_year);
assert_eq!(round_trip.resource_type_general, output.resource_type_general);
assert_eq!(round_trip.resource_type, output.resource_type);
assert_eq!(round_trip.language, output.language);
assert_eq!(round_trip.formats, output.formats);
assert_eq!(round_trip.sizes, output.sizes);
assert_eq!(round_trip.keywords, output.keywords);
assert_eq!(round_trip.creators.as_ref().unwrap()[0].ror, output.creators.as_ref().unwrap()[0].ror);
assert_eq!(
round_trip.contributors.as_ref().unwrap()[0].orcid,
output.contributors.as_ref().unwrap()[0].orcid
);
assert_eq!(
round_trip.funding.as_ref().unwrap()[0].identifier,
output.funding.as_ref().unwrap()[0].identifier
);
assert_eq!(
round_trip.funding.as_ref().unwrap()[0].awards[0].doi,
output.funding.as_ref().unwrap()[0].awards[0].doi
);
}
#[test]
fn test_rrid_crosswalk_preserves_relation_semantics_and_reports_local_category() {
let (related, warnings): (datacite::RelatedIdentifier, _) = rrid_related_resource().crosswalk().unwrap();
assert_eq!(related.related_identifier, "RRID:SCR_003512");
assert_eq!(related.related_identifier_type, Some(datacite::RelatedIdentifierType::Rrid));
assert_eq!(related.relation_type, Some(datacite::RelationType::Cites));
assert_eq!(related.relation_type_information.as_deref(), Some("Experimental software input"));
assert_eq!(related.resource_type_general, Some(datacite::ResourceTypeGeneral::Software));
assert_eq!(related.scheme_uri.as_deref(), Some("https://scicrunch.org/resolver"));
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].field, "relatedResources[].category");
assert_eq!(warnings[0].loss_type, FieldLossType::NoEquivalent);
}
#[test]
fn test_rrid_crosswalk_rejects_malformed_or_mistyped_identifiers() {
let malformed = TypedRelatedResource {
identifier: TypedIdentifier {
value: "RRID:".to_string(),
..rrid_related_resource().identifier
},
..rrid_related_resource()
};
let mistyped = TypedRelatedResource {
identifier: TypedIdentifier {
scheme: Some("DOI".to_string()),
..rrid_related_resource().identifier
},
..rrid_related_resource()
};
let invalid_relation = TypedRelatedResource {
relation_type: "Invented".to_string(),
..rrid_related_resource()
};
assert!(<TypedRelatedResource as Crosswalk<datacite::RelatedIdentifier>>::crosswalk(&malformed).is_err());
assert!(<TypedRelatedResource as Crosswalk<datacite::RelatedIdentifier>>::crosswalk(&mistyped).is_err());
assert!(<TypedRelatedResource as Crosswalk<datacite::RelatedIdentifier>>::crosswalk(&invalid_relation).is_err());
}
#[test]
fn test_rrid_crosswalk_round_trips_datacite_fixture_and_reports_unrepresented_fields() {
let content = include_str!("../../../../../../../tests/fixtures/schema/datacite/4.7/valid-4.7-deltas.json");
let records: Vec<datacite::Record> = serde_json::from_str(content).unwrap();
let related = records[0]
.attributes
.related_identifiers
.as_ref()
.unwrap()
.iter()
.find(|related| related.related_identifier_type == Some(datacite::RelatedIdentifierType::Rrid))
.unwrap();
let with_unrepresented = datacite::RelatedIdentifier {
related_metadata_scheme: Some("custom".to_string()),
scheme_type: Some("custom-type".to_string()),
..related.clone()
};
let (rad, warnings): (TypedRelatedResource, _) = with_unrepresented.crosswalk().unwrap();
assert_eq!(rad.identifier.value, "RRID:SCR_003512");
assert_eq!(rad.identifier.scheme.as_deref(), Some("RRID"));
assert_eq!(rad.relation_type, "Cites");
assert_eq!(rad.resource_type_general.as_deref(), Some("Software"));
assert_eq!(warnings.len(), 2);
assert!(warnings.iter().all(|warning| warning.loss_type == FieldLossType::NoEquivalent));
let (round_trip, warnings): (datacite::RelatedIdentifier, _) = rad.crosswalk().unwrap();
assert_eq!(round_trip.related_identifier, related.related_identifier);
assert_eq!(round_trip.related_identifier_type, related.related_identifier_type);
assert_eq!(round_trip.relation_type, related.relation_type);
assert_eq!(round_trip.resource_type_general, related.resource_type_general);
assert!(warnings.is_empty());
let missing_relation = datacite::RelatedIdentifier {
relation_type: None,
..related.clone()
};
assert!(matches!(
<datacite::RelatedIdentifier as Crosswalk<TypedRelatedResource>>::crosswalk(&missing_relation),
Err(CrosswalkError::MissingRequiredField(_))
));
}