use std::collections::HashMap;
use unicode_normalization::UnicodeNormalization;
use super::ValidationError;
use crate::entity::Entity;
pub fn check_unique_ids(entities: &[Entity]) -> Result<(), ValidationError> {
let mut seen: HashMap<String, &str> = HashMap::new();
for entity in entities {
let nfc: String = entity.id.as_ref().nfc().collect();
if let Some(prev_path) = seen.get(&nfc) {
return Err(ValidationError::DuplicateEntityId {
id: nfc,
paths: ((*prev_path).to_string(), entity.file_path.clone()),
});
}
seen.insert(nfc, entity.file_path.as_str());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entity::{Entity, EntityId};
use indexmap::IndexMap;
fn stub_entity(id: &str, file_path: &str) -> Entity {
Entity {
id: EntityId(id.to_string()),
title: String::new(),
entity_type: "spec".to_string(),
mem: "v".to_string(),
file_path: file_path.to_string(),
metadata: IndexMap::new(),
sections: IndexMap::new(),
relationships: Vec::new(),
content_hash: String::new(),
stub: false,
stub_kind: None,
heading_spans: std::collections::HashMap::new(),
}
}
#[test]
fn accepts_distinct_ids() {
let entities = vec![
stub_entity("v--a", "a.md"),
stub_entity("v--b", "b.md"),
stub_entity("v--a/child", "a/child.md"),
];
check_unique_ids(&entities).unwrap();
}
#[test]
fn rejects_ascii_duplicates() {
let entities = vec![
stub_entity("v--hello-world", "a.md"),
stub_entity("v--hello-world", "b.md"),
];
let err = check_unique_ids(&entities).unwrap_err();
match err {
ValidationError::DuplicateEntityId { id, paths } => {
assert_eq!(id, "v--hello-world");
assert_eq!(paths, ("a.md".to_string(), "b.md".to_string()));
}
other => panic!("expected DuplicateEntityId, got {other:?}"),
}
}
#[test]
fn rejects_nfc_nfd_duplicates() {
let entities = vec![
stub_entity("v--bj\u{00F6}rn", "nfc.md"),
stub_entity("v--bjo\u{0308}rn", "nfd.md"),
];
let err = check_unique_ids(&entities).unwrap_err();
assert!(matches!(err, ValidationError::DuplicateEntityId { .. }));
}
#[test]
fn accepts_empty() {
check_unique_ids(&[]).unwrap();
}
}