Skip to main content

datasynth_group/
resolve.rs

1//! Three-level inheritance: defaults → scoping_profile → per-entity override.
2//! Produces a [`ResolvedEntity`] with every field the orchestrator needs.
3
4use crate::config::{EntityConfig, GroupConfig};
5use crate::errors::{GroupError, GroupResult};
6
7/// Fully resolved entity ready for shard execution.
8#[derive(Debug, Clone)]
9pub struct ResolvedEntity {
10    pub code: String,
11    pub name: Option<String>,
12    pub country: String,
13    pub functional_currency: String,
14    pub scoping_profile_name: String,
15    pub consolidation_method: crate::config::ConsolidationMethod,
16    pub ownership_percent: Option<rust_decimal::Decimal>,
17    pub parent_code: Option<String>,
18    pub accounting_framework: String,
19    pub industry: String,
20    pub process_models: Vec<String>,
21    pub rows: Option<u64>,
22    /// The merged YAML map of defaults ⊕ scoping_profile ⊕ per-entity overrides.
23    /// Downstream consumers project their required keys out of this (e.g. audit, tax).
24    pub merged_config: serde_yaml::Value,
25}
26
27/// Resolve a single entity by code.
28pub fn resolve_entity(cfg: &GroupConfig, code: &str) -> GroupResult<ResolvedEntity> {
29    let entity = cfg
30        .ownership
31        .entities
32        .iter()
33        .find(|e| e.code == code)
34        .ok_or_else(|| GroupError::Config(format!("entity {code} not found")))?;
35    resolve_entity_inner(cfg, entity)
36}
37
38fn resolve_entity_inner(cfg: &GroupConfig, entity: &EntityConfig) -> GroupResult<ResolvedEntity> {
39    // Start from defaults (may be serde_yaml::Value::Null if absent).
40    let mut merged = cfg.defaults.clone();
41    if merged.is_null() {
42        merged = serde_yaml::Value::Mapping(Default::default());
43    }
44
45    // Layer the scoping profile.
46    let profile = cfg
47        .scoping_profiles
48        .get(&entity.scoping_profile)
49        .ok_or_else(|| {
50            GroupError::Config(format!(
51                "entity {} references unknown scoping_profile {}",
52                entity.code, entity.scoping_profile
53            ))
54        })?;
55    deep_merge(&mut merged, profile);
56
57    // Layer per-entity overrides (the `#[serde(default, flatten)]` map).
58    for (k, v) in &entity.overrides {
59        deep_merge_key(&mut merged, k, v);
60    }
61
62    // Pull fields out with fallbacks. Named EntityConfig fields win over merged map when set.
63    let accounting_framework = entity
64        .accounting_framework
65        .clone()
66        .or_else(|| read_str(&merged, "accounting_framework"))
67        .unwrap_or_else(|| "ifrs".to_string());
68    let industry = entity
69        .industry
70        .clone()
71        .or_else(|| read_str(&merged, "industry"))
72        .unwrap_or_else(|| "manufacturing".to_string());
73    let process_models = read_str_vec(&merged, "process_models").unwrap_or_default();
74
75    Ok(ResolvedEntity {
76        code: entity.code.clone(),
77        name: entity.name.clone(),
78        country: entity.country.clone(),
79        functional_currency: entity.functional_currency.clone(),
80        scoping_profile_name: entity.scoping_profile.clone(),
81        consolidation_method: entity.consolidation_method,
82        ownership_percent: entity.ownership_percent,
83        parent_code: entity.parent_code.clone(),
84        accounting_framework,
85        industry,
86        process_models,
87        rows: entity.rows,
88        merged_config: merged,
89    })
90}
91
92/// Deep-merge `overlay` into `base` (overlay wins on scalar conflicts; maps merge recursively;
93/// sequences are replaced, not concatenated).
94fn deep_merge(base: &mut serde_yaml::Value, overlay: &serde_yaml::Value) {
95    use serde_yaml::Value::Mapping;
96    match (base, overlay) {
97        (Mapping(bm), Mapping(om)) => {
98            for (k, v) in om {
99                if let Some(bv) = bm.get_mut(k) {
100                    deep_merge(bv, v);
101                } else {
102                    bm.insert(k.clone(), v.clone());
103                }
104            }
105        }
106        (b, o) => *b = o.clone(),
107    }
108}
109
110fn deep_merge_key(base: &mut serde_yaml::Value, key: &str, value: &serde_yaml::Value) {
111    let k = serde_yaml::Value::String(key.into());
112    match base {
113        serde_yaml::Value::Mapping(m) => {
114            if let Some(existing) = m.get_mut(&k) {
115                deep_merge(existing, value);
116            } else {
117                m.insert(k, value.clone());
118            }
119        }
120        _ => {
121            let mut m = serde_yaml::Mapping::new();
122            m.insert(k, value.clone());
123            *base = serde_yaml::Value::Mapping(m);
124        }
125    }
126}
127
128fn read_str(v: &serde_yaml::Value, key: &str) -> Option<String> {
129    v.as_mapping()?
130        .get(serde_yaml::Value::String(key.into()))?
131        .as_str()
132        .map(String::from)
133}
134
135fn read_str_vec(v: &serde_yaml::Value, key: &str) -> Option<Vec<String>> {
136    let seq = v
137        .as_mapping()?
138        .get(serde_yaml::Value::String(key.into()))?
139        .as_sequence()?;
140    Some(
141        seq.iter()
142            .filter_map(|x| x.as_str().map(String::from))
143            .collect(),
144    )
145}