memstead_base/validator/
ids.rs1use std::collections::HashMap;
11use unicode_normalization::UnicodeNormalization;
12
13use super::ValidationError;
14use crate::entity::Entity;
15
16pub fn check_unique_ids(entities: &[Entity]) -> Result<(), ValidationError> {
20 let mut seen: HashMap<String, &str> = HashMap::new();
21 for entity in entities {
22 let nfc: String = entity.id.as_ref().nfc().collect();
23 if let Some(prev_path) = seen.get(&nfc) {
24 return Err(ValidationError::DuplicateEntityId {
25 id: nfc,
26 paths: ((*prev_path).to_string(), entity.file_path.clone()),
27 });
28 }
29 seen.insert(nfc, entity.file_path.as_str());
30 }
31 Ok(())
32}
33
34#[cfg(test)]
35mod tests {
36 use super::*;
37 use crate::entity::{Entity, EntityId};
38 use indexmap::IndexMap;
39
40 fn stub_entity(id: &str, file_path: &str) -> Entity {
41 Entity {
42 id: EntityId(id.to_string()),
43 title: String::new(),
44 entity_type: "spec".to_string(),
45 mem: "v".to_string(),
46 file_path: file_path.to_string(),
47 metadata: IndexMap::new(),
48 sections: IndexMap::new(),
49 relationships: Vec::new(),
50 content_hash: String::new(),
51 stub: false,
52 stub_kind: None,
53 heading_spans: std::collections::HashMap::new(),
54 raw_section_headings: Vec::new(),
55 }
56 }
57
58 #[test]
59 fn accepts_distinct_ids() {
60 let entities = vec![
61 stub_entity("v--a", "a.md"),
62 stub_entity("v--b", "b.md"),
63 stub_entity("v--a/child", "a/child.md"),
64 ];
65 check_unique_ids(&entities).unwrap();
66 }
67
68 #[test]
69 fn rejects_ascii_duplicates() {
70 let entities = vec![
71 stub_entity("v--hello-world", "a.md"),
72 stub_entity("v--hello-world", "b.md"),
73 ];
74 let err = check_unique_ids(&entities).unwrap_err();
75 match err {
76 ValidationError::DuplicateEntityId { id, paths } => {
77 assert_eq!(id, "v--hello-world");
78 assert_eq!(paths, ("a.md".to_string(), "b.md".to_string()));
79 }
80 other => panic!("expected DuplicateEntityId, got {other:?}"),
81 }
82 }
83
84 #[test]
85 fn rejects_nfc_nfd_duplicates() {
86 let entities = vec![
90 stub_entity("v--bj\u{00F6}rn", "nfc.md"),
91 stub_entity("v--bjo\u{0308}rn", "nfd.md"),
92 ];
93 let err = check_unique_ids(&entities).unwrap_err();
94 assert!(matches!(err, ValidationError::DuplicateEntityId { .. }));
95 }
96
97 #[test]
98 fn accepts_empty() {
99 check_unique_ids(&[]).unwrap();
100 }
101}