use std::collections::{BTreeMap, BTreeSet};
pub(super) fn stems(paths: &BTreeSet<String>) -> BTreeMap<String, String> {
let mut claimants: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for path in paths {
claimants.entry(bare(path)).or_default().push(path);
}
let mut taken: BTreeSet<String> = BTreeSet::new();
let mut assigned: BTreeMap<String, String> = BTreeMap::new();
for (name, paths) in claimants.iter().filter(|(_, p)| p.len() == 1) {
taken.insert((*name).to_string());
assigned.insert(paths[0].to_string(), (*name).to_string());
}
for (_, paths) in claimants.iter().filter(|(_, p)| p.len() > 1) {
for path in paths {
let chosen = pick(path, &taken);
taken.insert(chosen.clone());
assigned.insert((*path).to_string(), chosen);
}
}
assigned
}
fn pick(path: &str, taken: &BTreeSet<String>) -> String {
let segs: Vec<&str> = path.split("::").collect();
for n in 1..=segs.len() {
let candidate = segs[segs.len() - n..].join("_");
if !taken.contains(&candidate) {
return candidate;
}
}
let stem = segs.join("_");
(2..)
.map(|n| format!("{stem}_{n}"))
.find(|c| !taken.contains(c))
.unwrap_or(stem)
}
fn bare(path: &str) -> &str {
path.rsplit("::").next().unwrap_or(path)
}
#[cfg(test)]
mod tests {
use super::*;
fn set(paths: &[&str]) -> BTreeSet<String> {
paths.iter().map(|p| (*p).to_string()).collect()
}
#[test]
fn an_unambiguous_name_stays_bare() {
let s = stems(&set(&["app::model::Item", "app::model::Order"]));
assert_eq!(s["app::model::Item"], "Item");
assert_eq!(s["app::model::Order"], "Order");
}
#[test]
fn two_same_named_types_in_one_crate_are_told_apart_by_module() {
let s = stems(&set(&[
"finance_query::constants::Region",
"finance_query::constants::indices::Region",
]));
assert_eq!(s["finance_query::constants::Region"], "Region");
assert_eq!(
s["finance_query::constants::indices::Region"],
"indices_Region"
);
}
#[test]
fn same_named_types_in_different_crates_are_told_apart_too() {
let s = stems(&set(&["server::Meta", "lib_crate::Meta"]));
assert_eq!(s["lib_crate::Meta"], "Meta", "sorts first, keeps the name");
assert_eq!(s["server::Meta"], "server_Meta");
}
#[test]
fn the_assignment_does_not_depend_on_input_order() {
let forward = stems(&set(&[
"a::x::Region",
"b::Region",
"a::y::Region",
"a::Item",
]));
let backward = stems(&set(&[
"a::Item",
"a::y::Region",
"b::Region",
"a::x::Region",
]));
assert_eq!(forward, backward);
let names: BTreeSet<&String> = forward.values().collect();
assert_eq!(names.len(), forward.len());
}
#[test]
fn qualifying_never_steals_a_name_another_type_owns() {
let s = stems(&set(&[
"app::Region",
"app::indices::Region",
"app::indices_Region",
]));
assert_eq!(s["app::indices_Region"], "indices_Region");
assert_eq!(s["app::Region"], "Region");
assert_eq!(s["app::indices::Region"], "app_indices_Region");
}
#[test]
fn three_claimants_all_get_distinct_names() {
let s = stems(&set(&["a::R", "b::R", "c::R"]));
let names: BTreeSet<&String> = s.values().collect();
assert_eq!(names.len(), 3, "got {s:?}");
assert_eq!(s["a::R"], "R", "the smallest path keeps the bare name");
}
#[test]
fn a_single_segment_path_is_handled() {
let s = stems(&set(&["Region", "app::Region"]));
assert_eq!(s["Region"], "Region");
assert_eq!(s["app::Region"], "app_Region");
}
}