greentic-flow-builder 0.1.0

AI-powered Adaptive Card flow builder with visual graph editor and demo runner
Documentation
//! Theme overlay logic: base + override → merged theme.

use crate::template::Theme;
use serde_json::Value;

/// Merge two themes. `override_theme` overlays `base`.
/// Token keys in override win. Host config is deep-merged.
pub fn merge_themes(base: &Theme, override_theme: &Theme) -> Theme {
    let mut merged = base.clone();
    merged.name = override_theme.name.clone();
    merged.display_name = override_theme.display_name.clone();
    merged.description = override_theme.description.clone();

    for (key, value) in &override_theme.tokens {
        merged.tokens.insert(key.clone(), value.clone());
    }

    merged.host_config = deep_merge_json(&base.host_config, &override_theme.host_config);

    merged
}

fn deep_merge_json(base: &Value, overlay: &Value) -> Value {
    match (base, overlay) {
        (Value::Object(base_map), Value::Object(overlay_map)) => {
            let mut merged = base_map.clone();
            for (key, value) in overlay_map {
                merged
                    .entry(key.clone())
                    .and_modify(|existing| *existing = deep_merge_json(existing, value))
                    .or_insert_with(|| value.clone());
            }
            Value::Object(merged)
        }
        (_, overlay) if !overlay.is_null() => overlay.clone(),
        (base, _) => base.clone(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::BTreeMap;

    fn theme(name: &str, tokens: &[(&str, Value)]) -> Theme {
        let mut map = BTreeMap::new();
        for (k, v) in tokens {
            map.insert(k.to_string(), v.clone());
        }
        Theme {
            name: name.to_string(),
            display_name: name.to_string(),
            description: String::new(),
            tokens: map,
            host_config: Value::Null,
        }
    }

    #[test]
    fn test_merge_tokens_override_wins() {
        let base = theme("base", &[("color", Value::from("#000"))]);
        let overlay = theme("overlay", &[("color", Value::from("#fff"))]);
        let merged = merge_themes(&base, &overlay);
        assert_eq!(merged.tokens["color"], "#fff");
    }

    #[test]
    fn test_merge_keeps_base_tokens_not_in_overlay() {
        let base = theme(
            "base",
            &[("color", Value::from("#000")), ("spacing", Value::from(12))],
        );
        let overlay = theme("overlay", &[("color", Value::from("#fff"))]);
        let merged = merge_themes(&base, &overlay);
        assert_eq!(merged.tokens["color"], "#fff");
        assert_eq!(merged.tokens["spacing"], 12);
    }

    #[test]
    fn test_merge_preserves_overlay_name() {
        let base = theme("base", &[]);
        let overlay = theme("corporate", &[]);
        let merged = merge_themes(&base, &overlay);
        assert_eq!(merged.name, "corporate");
    }
}