use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::path_resolver::PathResolver;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MappingDefinition {
pub meta: MappingMeta,
pub fields: IndexMap<String, FieldMapping>,
pub companion_fields: Option<IndexMap<String, FieldMapping>>,
pub complex_handlers: Option<Vec<ComplexHandlerRef>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MappingMeta {
pub entity: String,
pub bo4e_type: String,
pub companion_type: Option<String>,
pub source_group: String,
#[serde(default)]
pub source_path: Option<String>,
pub discriminator: Option<String>,
pub repeat_on_tag: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FieldMapping {
Simple(String),
Structured(StructuredFieldMapping),
Nested(IndexMap<String, FieldMapping>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructuredFieldMapping {
pub target: String,
pub transform: Option<String>,
pub when: Option<String>,
pub default: Option<String>,
pub enum_map: Option<BTreeMap<String, String>>,
pub when_filled: Option<Vec<String>>,
pub also_target: Option<String>,
pub also_enum_map: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexHandlerRef {
pub name: String,
pub description: Option<String>,
}
impl MappingDefinition {
pub fn normalize_paths(&mut self, resolver: &PathResolver) {
if let Some(ref disc) = self.meta.discriminator {
self.meta.discriminator = Some(resolver.resolve_discriminator(disc));
}
self.fields = self
.fields
.iter()
.map(|(k, v)| (resolver.resolve_path(k), v.clone()))
.collect();
if let Some(ref cf) = self.companion_fields {
self.companion_fields = Some(
cf.iter()
.map(|(k, v)| (resolver.resolve_path(k), v.clone()))
.collect(),
);
}
}
pub fn validate(&self) -> Vec<String> {
let mut warnings = Vec::new();
let mut field_targets: Vec<&str> = Vec::new();
for fm in self.fields.values() {
if let Some(t) = extract_target_ref(fm) {
if !t.is_empty() {
field_targets.push(t);
}
}
}
if let Some(ref cf) = self.companion_fields {
for fm in cf.values() {
if let Some(t) = extract_target_ref(fm) {
if !t.is_empty() && field_targets.contains(&t) {
warnings.push(format!(
"field '{}' appears in both [fields] and [companion_fields] in entity '{}'",
t, self.meta.entity
));
}
}
}
}
warnings
}
}
fn extract_target_ref(fm: &FieldMapping) -> Option<&str> {
match fm {
FieldMapping::Simple(t) => Some(t.as_str()),
FieldMapping::Structured(s) => Some(s.target.as_str()),
FieldMapping::Nested(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_when_filled_deserialization() {
let toml = r#"
[meta]
entity = "Test"
bo4e_type = "Test"
source_group = "SG4.SG8.SG10"
[fields]
[companion_fields]
"cci.d7059" = { target = "", default = "Z83", when_filled = ["merkmal.code"] }
"cav.c889.d7111" = "merkmal.code"
"#;
let def: MappingDefinition = toml::from_str(toml).unwrap();
let cf = def.companion_fields.unwrap();
let cci = &cf["cci.d7059"];
match cci {
FieldMapping::Structured(s) => {
assert_eq!(s.target, "");
assert_eq!(s.default.as_deref(), Some("Z83"));
let wf = s.when_filled.as_ref().unwrap();
assert_eq!(wf, &vec!["merkmal.code".to_string()]);
}
_ => panic!("expected Structured variant"),
}
}
#[test]
fn test_validate_duplicate_target_warning() {
let toml = r#"
[meta]
entity = "Test"
bo4e_type = "Test"
companion_type = "TestEdifact"
source_group = "SG4"
[fields]
"loc.1.0" = "someField"
[companion_fields]
"loc.0.0" = "someField"
"#;
let def: MappingDefinition = toml::from_str(toml).unwrap();
let warnings = def.validate();
assert!(!warnings.is_empty());
assert!(warnings[0].contains("someField"));
}
#[test]
fn test_validate_no_warnings_for_clean_def() {
let toml = r#"
[meta]
entity = "Test"
bo4e_type = "Test"
source_group = "SG4"
[fields]
"loc.1.0" = "marktlokationsId"
[companion_fields]
"loc.0.0" = { target = "", default = "Z16", when_filled = ["marktlokationsId"] }
"#;
let def: MappingDefinition = toml::from_str(toml).unwrap();
let warnings = def.validate();
assert!(warnings.is_empty());
}
#[test]
fn test_when_filled_absent_is_none() {
let toml = r#"
[meta]
entity = "Test"
bo4e_type = "Test"
source_group = "SG4"
[fields]
"loc.d3227" = { target = "", default = "Z16" }
"#;
let def: MappingDefinition = toml::from_str(toml).unwrap();
let loc = &def.fields["loc.d3227"];
match loc {
FieldMapping::Structured(s) => {
assert!(s.when_filled.is_none());
}
_ => panic!("expected Structured variant"),
}
}
}