use acorn_core::prelude::alloc::{format, String, Vec};
use core::fmt;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
pub mod mapping;
mod research_activity;
mod rrid;
pub use mapping::{FieldMapping, FieldRule, FieldValue, Fields};
pub use research_activity::{RaidExportOptions, RaidImportOptions};
pub trait Crosswalk<T>: Sized {
fn crosswalk(&self) -> Result<(T, Vec<ConversionWarning>), CrosswalkError>;
}
pub trait SchemaBuilder: Sized {
fn build_from_fields(fields: &Fields) -> Result<Self, CrosswalkError>;
}
pub trait SchemaExtractor {
fn extract_fields(&self) -> Fields;
}
#[derive(Clone, Debug)]
pub enum CrosswalkError {
MissingRequiredField(String),
TransformationFailed {
field: String,
reason: String,
},
BuildFailed(String),
ParseFailed(String),
SerializeFailed(String),
}
#[derive(Clone, Copy, Debug)]
enum CrosswalkSchema {
DataCite,
Rad,
RadOutput,
Raid,
}
#[derive(Clone, Copy, Debug, Deserialize, Display, EnumString, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum CrosswalkStandard {
Datacite,
Dcat,
Huwise,
Invenio,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FieldLossType {
NoEquivalent,
Approximated,
Simplified,
Omitted,
BlockNotSelected,
}
#[derive(Clone, Debug)]
pub struct Conversion<T> {
value: T,
warnings: Vec<ConversionWarning>,
}
#[derive(Clone, Debug)]
pub struct ConversionWarning {
pub from: &'static str,
pub to: &'static str,
pub field: String,
pub loss_type: FieldLossType,
pub details: Option<String>,
}
impl<T> Conversion<T> {
pub fn new(value: T, warnings: Vec<ConversionWarning>) -> Self {
Self { value, warnings }
}
pub fn into_parts(self) -> (T, Vec<ConversionWarning>) {
(self.value, self.warnings)
}
pub fn into_value(self) -> T {
self.value
}
pub fn value(&self) -> &T {
&self.value
}
pub fn warnings(&self) -> &[ConversionWarning] {
&self.warnings
}
}
impl<T> From<(T, Vec<ConversionWarning>)> for Conversion<T> {
fn from((value, warnings): (T, Vec<ConversionWarning>)) -> Self {
Self::new(value, warnings)
}
}
impl ConversionWarning {
pub fn no_equivalent(from: impl Into<&'static str>, to: impl Into<&'static str>, field: impl Into<String>) -> Self {
Self {
from: from.into(),
to: to.into(),
field: field.into(),
details: None,
loss_type: FieldLossType::NoEquivalent,
}
}
pub fn approximated(from: impl Into<&'static str>, to: impl Into<&'static str>, field: impl Into<String>, details: impl Into<String>) -> Self {
Self {
from: from.into(),
to: to.into(),
field: field.into(),
details: Some(details.into()),
loss_type: FieldLossType::Approximated,
}
}
pub fn simplified(from: impl Into<&'static str>, to: impl Into<&'static str>, field: impl Into<String>, details: impl Into<String>) -> Self {
Self {
from: from.into(),
to: to.into(),
field: field.into(),
details: Some(details.into()),
loss_type: FieldLossType::Simplified,
}
}
pub fn omitted(from: impl Into<&'static str>, to: impl Into<&'static str>, field: impl Into<String>, details: impl Into<String>) -> Self {
Self {
from: from.into(),
to: to.into(),
field: field.into(),
details: Some(details.into()),
loss_type: FieldLossType::Omitted,
}
}
pub fn rebase(self, from: impl Into<&'static str>, fields: &[(&str, &str)]) -> Self {
let field = fields
.iter()
.find_map(|(source, target)| self.field.strip_prefix(source).map(|suffix| format!("{target}{suffix}")))
.unwrap_or(self.field);
Self {
from: from.into(),
field,
..self
}
}
}
impl fmt::Display for ConversionWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} → {}: {} ({}) {}",
self.from,
self.to,
self.field,
self.loss_type,
self.details.as_deref().unwrap_or("")
)
}
}
impl fmt::Display for CrosswalkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
| Self::MissingRequiredField(field) => write!(f, "missing required field — {field}"),
| Self::TransformationFailed { field, reason } => {
write!(f, "failed to transform field '{field}' — {reason}")
}
| Self::BuildFailed(reason) => write!(f, "build failed — {reason}"),
| Self::ParseFailed(reason) => write!(f, "content parse failed — {reason}"),
| Self::SerializeFailed(reason) => write!(f, "output serialize failed — {reason}"),
}
}
}
impl From<String> for CrosswalkError {
fn from(err: String) -> Self {
CrosswalkError::BuildFailed(err)
}
}
impl fmt::Display for FieldLossType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
| Self::NoEquivalent => write!(f, "no equivalent field"),
| Self::Approximated => write!(f, "approximated"),
| Self::Simplified => write!(f, "simplified"),
| Self::Omitted => write!(f, "omitted"),
| Self::BlockNotSelected => write!(f, "metadata block not selected"),
}
}
}
impl From<CrosswalkSchema> for &'static str {
fn from(schema: CrosswalkSchema) -> Self {
match schema {
| CrosswalkSchema::DataCite => "datacite",
| CrosswalkSchema::Rad => "rad",
| CrosswalkSchema::RadOutput => "rad-output",
| CrosswalkSchema::Raid => "raid",
}
}
}
pub fn convert<S, T>(source: &S, mapping: &FieldMapping) -> Result<(T, Vec<ConversionWarning>), CrosswalkError>
where
S: SchemaExtractor,
T: SchemaBuilder,
{
let source_fields = source.extract_fields();
let mut target_fields = Fields::new();
mapping.apply(&source_fields, &mut target_fields).and_then(|missing| {
let warnings = missing
.into_iter()
.map(|field| {
ConversionWarning::omitted(
mapping.source,
mapping.target,
field.clone(),
format!("optional '{field}' not present in source; target '{}' remains empty", field),
)
})
.collect();
T::build_from_fields(&target_fields).map(|target| (target, warnings))
})
}