use std::collections::HashMap;
pub fn split_tag_list(input: &str) -> Vec<String> {
input
.split(',')
.filter(|tag| !tag.is_empty())
.map(|tag| tag.to_string())
.collect()
}
pub fn tag_display_name(tag: &str) -> &str {
if let Some(separator) = tag.rfind('|')
&& is_hex_color(&tag[separator + 1..])
{
return &tag[..separator];
}
tag
}
fn is_hex_color(text: &str) -> bool {
text.len() == 6 && text.bytes().all(|b| b.is_ascii_hexdigit())
}
pub fn normalize_tags<I, S>(tags: I) -> Vec<String>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut by_display_name: HashMap<String, String> = HashMap::new();
for tag in tags {
let tag = tag.as_ref();
by_display_name.insert(tag_display_name(tag).to_string(), tag.to_string());
}
let mut result: Vec<String> = by_display_name.into_values().collect();
result.sort();
result
}
fn variant_index(pool: &[String]) -> HashMap<String, String> {
let mut sorted = pool.to_vec();
sorted.sort();
let mut index: HashMap<String, String> = HashMap::new();
for tag in sorted {
index
.entry(tag_display_name(&tag).to_string())
.or_insert(tag);
}
index
}
fn resolve_variant(name: &str, known: &HashMap<String, String>) -> String {
known
.get(tag_display_name(name))
.cloned()
.unwrap_or_else(|| name.to_string())
}
pub fn set_tags<I, S>(requested: I, known: &[String]) -> Vec<String>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let known = variant_index(known);
normalize_tags(
requested
.into_iter()
.map(|tag| resolve_variant(tag.as_ref(), &known)),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_name_strips_color_suffix() {
assert_eq!(tag_display_name("prod"), "prod");
assert_eq!(tag_display_name("prod|ef4444"), "prod");
assert_eq!(tag_display_name("team billing|22c55e"), "team billing");
assert_eq!(tag_display_name("prod|ABCDEF"), "prod");
assert_eq!(tag_display_name("a|b"), "a|b");
assert_eq!(tag_display_name("env|staging"), "env|staging");
assert_eq!(tag_display_name("prod|fff"), "prod|fff");
assert_eq!(tag_display_name("prod|gggggg"), "prod|gggggg");
assert_eq!(tag_display_name("a|b|ef4444"), "a|b");
assert_eq!(tag_display_name(""), "");
}
#[test]
fn sorts_lexicographically() {
assert_eq!(
normalize_tags(["prod", "dev", "staging"]),
vec!["dev", "prod", "staging"]
);
}
#[test]
fn is_idempotent() {
let once = normalize_tags(["beta", "alpha", "alpha|ef4444"]);
assert_eq!(normalize_tags(&once), once);
}
#[test]
fn empty_input() {
assert_eq!(normalize_tags(Vec::<String>::new()), Vec::<String>::new());
}
#[test]
fn collapses_color_variants_keeping_last() {
assert_eq!(normalize_tags(["prod", "prod|ef4444"]), vec!["prod|ef4444"]);
assert_eq!(normalize_tags(["prod|ef4444", "prod"]), vec!["prod"]);
}
#[test]
fn preserves_color_of_survivor() {
assert_eq!(
normalize_tags(["dev", "prod|ef4444", "qa"]),
vec!["dev", "prod|ef4444", "qa"]
);
}
#[test]
fn distinct_names_with_colors_all_kept() {
assert_eq!(
normalize_tags(["b|ef4444", "a|22c55e", "c"]),
vec!["a|22c55e", "b|ef4444", "c"]
);
}
#[test]
fn split_drops_empty_segments_and_keeps_spaces() {
assert_eq!(split_tag_list(""), Vec::<String>::new());
assert_eq!(split_tag_list("a,b,c"), vec!["a", "b", "c"]);
assert_eq!(split_tag_list("a,,b,"), vec!["a", "b"]);
assert_eq!(
split_tag_list("team billing,prod"),
vec!["team billing", "prod"]
);
}
#[test]
fn set_borrows_color_from_known_pool() {
assert_eq!(
set_tags(["prod", "dev"], &["prod|ef4444".to_string()]),
vec!["dev", "prod|ef4444"]
);
}
#[test]
fn set_color_conflict_is_deterministic() {
assert_eq!(
set_tags(
["prod"],
&["prod|ef4444".to_string(), "prod|22c55e".to_string()]
),
vec!["prod|22c55e"]
);
}
#[test]
fn set_adds_uncolored_name_when_absent() {
assert_eq!(set_tags(["prod"], &[]), vec!["prod"]);
}
#[test]
fn set_with_no_tags_clears() {
assert_eq!(set_tags(Vec::<String>::new(), &[]), Vec::<String>::new());
}
}