use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConflictType {
DuplicateId,
ReservedWord,
CaseCollision,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConflictSeverity {
Error,
Warning,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConflictResult {
pub conflict_type: ConflictType,
pub severity: ConflictSeverity,
pub message: String,
}
#[allow(clippy::implicit_hasher)] #[must_use]
pub fn detect_id_conflicts(
new_id: &str,
existing_ids: &HashSet<String>,
reserved_words: &[&str],
lowercase_map: Option<&HashMap<String, String>>,
) -> Option<ConflictResult> {
if existing_ids.contains(new_id) {
return Some(ConflictResult {
conflict_type: ConflictType::DuplicateId,
severity: ConflictSeverity::Error,
message: format!("Module ID '{new_id}' is already registered"),
});
}
if let Some(first_segment) = new_id.split('.').next() {
if reserved_words.contains(&first_segment) {
return Some(ConflictResult {
conflict_type: ConflictType::ReservedWord,
severity: ConflictSeverity::Error,
message: format!("Module ID '{new_id}' contains reserved word '{first_segment}'"),
});
}
}
let normalized_new = new_id.to_lowercase();
if let Some(lc_map) = lowercase_map {
if let Some(existing) = lc_map.get(&normalized_new) {
if existing != new_id {
return Some(ConflictResult {
conflict_type: ConflictType::CaseCollision,
severity: ConflictSeverity::Warning,
message: format!(
"Module ID '{new_id}' has a case collision with existing '{existing}'"
),
});
}
}
} else {
for existing_id in existing_ids {
if existing_id.to_lowercase() == normalized_new && existing_id != new_id {
return Some(ConflictResult {
conflict_type: ConflictType::CaseCollision,
severity: ConflictSeverity::Warning,
message: format!(
"Module ID '{new_id}' has a case collision with existing '{existing_id}'"
),
});
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_conflict() {
let existing: HashSet<String> = ["foo.bar".to_string()].into_iter().collect();
let reserved = &["system", "internal"];
assert!(detect_id_conflicts("baz.qux", &existing, reserved, None).is_none());
}
#[test]
fn test_duplicate_id() {
let existing: HashSet<String> = ["foo.bar".to_string()].into_iter().collect();
let result = detect_id_conflicts("foo.bar", &existing, &[], None).unwrap();
assert_eq!(result.conflict_type, ConflictType::DuplicateId);
assert_eq!(result.severity, ConflictSeverity::Error);
}
#[test]
fn test_reserved_word() {
let existing: HashSet<String> = HashSet::new();
let reserved = &["system", "internal"];
let result = detect_id_conflicts("system.foo", &existing, reserved, None).unwrap();
assert_eq!(result.conflict_type, ConflictType::ReservedWord);
assert_eq!(result.severity, ConflictSeverity::Error);
}
#[test]
fn test_reserved_word_allowed_in_middle_segment() {
let existing: HashSet<String> = HashSet::new();
let reserved = &["system", "internal"];
assert!(detect_id_conflicts("email.system", &existing, reserved, None).is_none());
assert!(detect_id_conflicts("foo.internal.bar", &existing, reserved, None).is_none());
}
#[test]
fn test_case_collision_without_map() {
let existing: HashSet<String> = ["Foo.Bar".to_string()].into_iter().collect();
let result = detect_id_conflicts("foo.bar", &existing, &[], None).unwrap();
assert_eq!(result.conflict_type, ConflictType::CaseCollision);
assert_eq!(result.severity, ConflictSeverity::Warning);
}
#[test]
fn test_case_collision_with_map() {
let existing: HashSet<String> = ["Foo.Bar".to_string()].into_iter().collect();
let mut lc_map = HashMap::new();
lc_map.insert("foo.bar".to_string(), "Foo.Bar".to_string());
let result = detect_id_conflicts("foo.bar", &existing, &[], Some(&lc_map)).unwrap();
assert_eq!(result.conflict_type, ConflictType::CaseCollision);
}
}