streamd/src/localize/configuration.rs
Konstantin Fickel ed493cff29
All checks were successful
Continuous Integration / Lint, Check & Test (push) Successful in 1m38s
Continuous Integration / Build Package (push) Successful in 1m54s
refactor: rewrite in rust
2026-03-29 18:28:03 +02:00

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);
}
}