448 lines
15 KiB
Rust
448 lines
15 KiB
Rust
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<String, Dimension>,
|
|
second: &IndexMap<String, Dimension>,
|
|
) -> IndexMap<String, Dimension> {
|
|
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>, 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<MarkerPlacement> = Vec::new();
|
|
let mut seen: IndexMap<(BTreeSet<String>, 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<String, Marker>,
|
|
second: &IndexMap<String, Marker>,
|
|
) -> IndexMap<String, Marker> {
|
|
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<_>>(),
|
|
vec!["A"]
|
|
);
|
|
// Second placement (B, d) should be from base
|
|
assert_eq!(
|
|
merged.placements[1].if_with.iter().collect::<Vec<_>>(),
|
|
vec!["B"]
|
|
);
|
|
// Third placement (C, d) should be from second
|
|
assert_eq!(
|
|
merged.placements[2].if_with.iter().collect::<Vec<_>>(),
|
|
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);
|
|
}
|
|
}
|