use std::collections::BTreeSet; use indexmap::IndexMap; use crate::models::{Dimension, Marker, MarkerPlacement, RepositoryConfiguration}; /// Merge two dimensions, with the second taking precedence. /// /// - display_name: second wins if non-empty, else base /// - comment: second wins if not None, else base /// - propagate: second wins if explicitly set, else base pub fn merge_single_dimension(base: &Dimension, second: &Dimension) -> Dimension { Dimension { display_name: if second.display_name.is_empty() { base.display_name.clone() } else { second.display_name.clone() }, comment: if second.comment.is_some() { second.comment.clone() } else { base.comment.clone() }, propagate: if second.propagate_was_set { second.propagate } else { base.propagate }, propagate_was_set: second.propagate_was_set || base.propagate_was_set, } } /// Merge two dimension maps. pub fn merge_dimensions( base: &IndexMap, second: &IndexMap, ) -> IndexMap { let mut merged = base.clone(); for (key, second_dim) in second { if let Some(base_dim) = merged.get(key) { merged.insert(key.clone(), merge_single_dimension(base_dim, second_dim)); } else { merged.insert(key.clone(), second_dim.clone()); } } merged } /// Create a placement identity tuple for deduplication. /// We use BTreeSet to make it hashable and order-independent. fn placement_identity(p: &MarkerPlacement) -> (BTreeSet, String) { (p.if_with.iter().cloned().collect(), p.dimension.clone()) } /// Merge two markers, with the second taking precedence. pub fn merge_single_marker(base: &Marker, second: &Marker) -> Marker { let display_name = if second.display_name.is_empty() { base.display_name.clone() } else { second.display_name.clone() }; let mut merged_placements: Vec = Vec::new(); let mut seen: IndexMap<(BTreeSet, String), usize> = IndexMap::new(); for placement in &base.placements { let ident = placement_identity(placement); seen.insert(ident, merged_placements.len()); merged_placements.push(placement.clone()); } for placement in &second.placements { let ident = placement_identity(placement); if let Some(&idx) = seen.get(&ident) { merged_placements[idx] = placement.clone(); } else { seen.insert(ident, merged_placements.len()); merged_placements.push(placement.clone()); } } Marker { display_name, placements: merged_placements, } } /// Merge two marker maps. pub fn merge_markers( base: &IndexMap, second: &IndexMap, ) -> IndexMap { let mut merged = base.clone(); for (key, second_marker) in second { if let Some(base_marker) = merged.get(key) { merged.insert(key.clone(), merge_single_marker(base_marker, second_marker)); } else { merged.insert(key.clone(), second_marker.clone()); } } merged } /// Merge two repository configurations. pub fn merge_repository_configuration( base: &RepositoryConfiguration, second: &RepositoryConfiguration, ) -> RepositoryConfiguration { RepositoryConfiguration { dimensions: merge_dimensions(&base.dimensions, &second.dimensions), markers: merge_markers(&base.markers, &second.markers), } } #[cfg(test)] mod tests { use super::*; #[test] fn test_second_overrides_display_name_when_non_empty() { let base = Dimension::new("Base") .with_comment("c1") .with_propagate(true); let second = Dimension::new("Second") .with_comment("c2") .with_propagate(false); let merged = merge_single_dimension(&base, &second); assert_eq!(merged.display_name, "Second"); assert_eq!(merged.comment, Some("c2".to_string())); assert!(!merged.propagate); } #[test] fn test_second_empty_display_name_falls_back_to_base() { let base = Dimension::new("Base") .with_comment("c1") .with_propagate(true); let second = Dimension::new("").with_comment("c2").with_propagate(false); let merged = merge_single_dimension(&base, &second); assert_eq!(merged.display_name, "Base"); assert_eq!(merged.comment, Some("c2".to_string())); assert!(!merged.propagate); } #[test] fn test_second_comment_none_does_not_erase_base_comment() { let base = Dimension::new("Base") .with_comment("keep") .with_propagate(true); let mut second = Dimension::new("Second"); second.propagate = false; second.propagate_was_set = true; let merged = merge_single_dimension(&base, &second); assert_eq!(merged.display_name, "Second"); assert_eq!(merged.comment, Some("keep".to_string())); } #[test] fn test_second_comment_non_none_overrides_base_comment() { let base = Dimension::new("Base") .with_comment("c1") .with_propagate(true); let second = Dimension::new("Second") .with_comment("c2") .with_propagate(true); let merged = merge_single_dimension(&base, &second); assert_eq!(merged.comment, Some("c2".to_string())); } #[test] fn test_second_propagate_overrides_base_when_provided() { let base = Dimension::new("Base") .with_comment("c1") .with_propagate(true); let second = Dimension::new("Second") .with_comment("c2") .with_propagate(false); let merged = merge_single_dimension(&base, &second); assert!(!merged.propagate); } #[test] fn test_propagate_merging_retains_base_when_second_not_provided() { let base = Dimension::new("Base") .with_comment("c1") .with_propagate(true); let second = Dimension::new("Second").with_comment("c2"); let merged = merge_single_dimension(&base, &second); assert!(merged.propagate); } #[test] fn test_adds_new_keys_from_second() { let mut base = IndexMap::new(); base.insert("a".to_string(), Dimension::new("A").with_propagate(true)); let mut second = IndexMap::new(); second.insert("b".to_string(), Dimension::new("B").with_propagate(false)); let merged = merge_dimensions(&base, &second); assert!(merged.contains_key("a")); assert!(merged.contains_key("b")); assert_eq!(merged["a"].display_name, "A"); assert_eq!(merged["b"].display_name, "B"); } #[test] fn test_merges_existing_keys() { let mut base = IndexMap::new(); base.insert( "a".to_string(), Dimension::new("A").with_comment("c1").with_propagate(true), ); let mut second = IndexMap::new(); second.insert("a".to_string(), Dimension::new("A2").with_propagate(false)); let merged = merge_dimensions(&base, &second); assert_eq!(merged["a"].display_name, "A2"); assert_eq!(merged["a"].comment, Some("c1".to_string())); assert!(!merged["a"].propagate); } #[test] fn test_does_not_mutate_inputs() { let mut base = IndexMap::new(); base.insert( "a".to_string(), Dimension::new("A").with_comment("c1").with_propagate(true), ); let mut second = IndexMap::new(); second.insert( "b".to_string(), Dimension::new("B").with_comment("c2").with_propagate(false), ); let merged = merge_dimensions(&base, &second); assert!(!base.contains_key("b")); assert!(!second.contains_key("a")); assert!(merged.contains_key("a")); assert!(merged.contains_key("b")); } #[test] fn test_second_marker_overrides_display_name_when_non_empty() { let base = Marker::new("Base").with_placements(vec![MarkerPlacement::new("project")]); let second = Marker::new("Second") .with_placements(vec![MarkerPlacement::new("timesheet").with_value("coding")]); let merged = merge_single_marker(&base, &second); assert_eq!(merged.display_name, "Second"); assert_eq!(merged.placements.len(), 2); assert_eq!(merged.placements[0].dimension, "project"); assert_eq!(merged.placements[1].dimension, "timesheet"); } #[test] fn test_second_marker_empty_display_name_falls_back_to_base() { let base = Marker::new("Base").with_placements(vec![]); let second = Marker::new("").with_placements(vec![]); let merged = merge_single_marker(&base, &second); assert_eq!(merged.display_name, "Base"); } #[test] fn test_appends_new_placements() { let base = Marker::new("Base").with_placements(vec![MarkerPlacement::new("project")]); let second = Marker::new("Second").with_placements(vec![MarkerPlacement::new("timesheet") .with_if_with(vec!["Timesheet"]) .with_value("x")]); let merged = merge_single_marker(&base, &second); assert_eq!(merged.placements.len(), 2); assert_eq!(merged.placements[0].dimension, "project"); assert_eq!(merged.placements[1].dimension, "timesheet"); } #[test] fn test_deduplicates_by_identity_and_second_overrides_base() { let base = Marker::new("Base").with_placements(vec![ MarkerPlacement::new("d") .with_if_with(vec!["A"]) .with_value("v"), MarkerPlacement::new("d") .with_if_with(vec!["B"]) .with_value("v2"), ]); let second = Marker::new("Second").with_placements(vec![ MarkerPlacement::new("d") .with_if_with(vec!["A"]) .with_value("v"), MarkerPlacement::new("d") .with_if_with(vec!["C"]) .with_value("v3"), ]); let merged = merge_single_marker(&base, &second); assert_eq!(merged.placements.len(), 3); // First placement (A, d) should be from second assert_eq!( merged.placements[0].if_with.iter().collect::>(), vec!["A"] ); // Second placement (B, d) should be from base assert_eq!( merged.placements[1].if_with.iter().collect::>(), vec!["B"] ); // Third placement (C, d) should be from second assert_eq!( merged.placements[2].if_with.iter().collect::>(), vec!["C"] ); } #[test] fn test_identity_is_order_insensitive_for_if_with() { let base = Marker::new("Base").with_placements(vec![MarkerPlacement::new("d") .with_if_with(vec!["A", "B"]) .with_value("v")]); let second = Marker::new("Second").with_placements(vec![MarkerPlacement::new("d") .with_if_with(vec!["B", "A"]) .with_value("v2")]); let merged = merge_single_marker(&base, &second); // With if_with as a set, identity is order-insensitive; second overrides base. assert_eq!(merged.placements.len(), 1); assert_eq!(merged.placements[0].value, Some("v2".to_string())); } #[test] fn test_adds_new_marker_keys_from_second() { let mut base = IndexMap::new(); base.insert("M1".to_string(), Marker::new("M1").with_placements(vec![])); let mut second = IndexMap::new(); second.insert("M2".to_string(), Marker::new("M2").with_placements(vec![])); let merged = merge_markers(&base, &second); assert!(merged.contains_key("M1")); assert!(merged.contains_key("M2")); } #[test] fn test_merges_existing_marker_keys() { let mut base = IndexMap::new(); base.insert( "M".to_string(), Marker::new("Base").with_placements(vec![MarkerPlacement::new("project")]), ); let mut second = IndexMap::new(); second.insert( "M".to_string(), Marker::new("Second").with_placements(vec![MarkerPlacement::new("timesheet") .with_if_with(vec!["Timesheet"]) .with_value("coding")]), ); let merged = merge_markers(&base, &second); assert_eq!(merged["M"].display_name, "Second"); assert_eq!(merged["M"].placements.len(), 2); } #[test] fn test_merge_repository_configuration() { let base = RepositoryConfiguration::new() .with_dimension( "project", Dimension::new("Project") .with_comment("c1") .with_propagate(true), ) .with_dimension( "moment", Dimension::new("Moment") .with_comment("c2") .with_propagate(true), ) .with_marker( "Streamd", Marker::new("Streamd").with_placements(vec![MarkerPlacement::new("project")]), ); let second = RepositoryConfiguration::new() .with_dimension("project", Dimension::new("Project2").with_propagate(false)) .with_dimension( "timesheet", Dimension::new("Timesheet") .with_comment("c3") .with_propagate(false), ) .with_marker( "Streamd", Marker::new("Streamd2").with_placements(vec![MarkerPlacement::new("timesheet") .with_if_with(vec!["Timesheet"]) .with_value("coding")]), ) .with_marker( "JobHunting", Marker::new("JobHunting").with_placements(vec![MarkerPlacement::new("project")]), ); let merged = merge_repository_configuration(&base, &second); assert!(merged.dimensions.contains_key("project")); assert!(merged.dimensions.contains_key("moment")); assert!(merged.dimensions.contains_key("timesheet")); assert_eq!(merged.dimensions["project"].display_name, "Project2"); assert_eq!(merged.dimensions["project"].comment, Some("c1".to_string())); assert!(!merged.dimensions["project"].propagate); assert_eq!(merged.dimensions["moment"].display_name, "Moment"); assert_eq!(merged.dimensions["timesheet"].display_name, "Timesheet"); assert!(merged.markers.contains_key("Streamd")); assert!(merged.markers.contains_key("JobHunting")); assert_eq!(merged.markers["Streamd"].display_name, "Streamd2"); assert_eq!(merged.markers["Streamd"].placements.len(), 2); } }