haystack_core/graph/
adjacency.rs1use smallvec::SmallVec;
4use std::collections::HashMap;
5
6pub struct RefAdjacency {
11 forward: HashMap<usize, SmallVec<[(String, String); 4]>>,
13 reverse: HashMap<String, SmallVec<[(String, usize); 4]>>,
15}
16
17impl RefAdjacency {
18 pub fn new() -> Self {
20 Self {
21 forward: HashMap::new(),
22 reverse: HashMap::new(),
23 }
24 }
25
26 pub fn add(&mut self, entity_id: usize, ref_tag: &str, target_ref_val: &str) {
28 self.forward
29 .entry(entity_id)
30 .or_default()
31 .push((ref_tag.to_string(), target_ref_val.to_string()));
32 self.reverse
33 .entry(target_ref_val.to_string())
34 .or_default()
35 .push((ref_tag.to_string(), entity_id));
36 }
37
38 pub fn remove(&mut self, entity_id: usize) {
42 if let Some(edges) = self.forward.remove(&entity_id) {
43 for (ref_tag, target) in edges {
44 if let Some(rev) = self.reverse.get_mut(&target) {
45 rev.retain(|(rt, sid)| !(rt == &ref_tag && *sid == entity_id));
46 if rev.is_empty() {
47 self.reverse.remove(&target);
48 }
49 }
50 }
51 }
52 }
53
54 pub fn targets_from(&self, entity_id: usize, ref_type: Option<&str>) -> Vec<String> {
59 match self.forward.get(&entity_id) {
60 Some(edges) => edges
61 .iter()
62 .filter(|(rt, _)| ref_type.is_none_or(|t| rt == t))
63 .map(|(_, target)| target.clone())
64 .collect(),
65 None => Vec::new(),
66 }
67 }
68
69 pub fn sources_to(&self, target_ref_val: &str, ref_type: Option<&str>) -> Vec<usize> {
72 match self.reverse.get(target_ref_val) {
73 Some(edges) => edges
74 .iter()
75 .filter(|(rt, _)| ref_type.is_none_or(|t| rt == t))
76 .map(|(_, sid)| *sid)
77 .collect(),
78 None => Vec::new(),
79 }
80 }
81}
82
83impl Default for RefAdjacency {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn add_forward_and_reverse_edges() {
95 let mut adj = RefAdjacency::new();
96 adj.add(0, "siteRef", "site-1");
97 adj.add(0, "equipRef", "equip-1");
98
99 let targets = adj.targets_from(0, None);
100 assert_eq!(targets.len(), 2);
101 assert!(targets.contains(&"site-1".to_string()));
102 assert!(targets.contains(&"equip-1".to_string()));
103
104 let sources = adj.sources_to("site-1", None);
105 assert_eq!(sources, vec![0]);
106 }
107
108 #[test]
109 fn remove_entity_edges() {
110 let mut adj = RefAdjacency::new();
111 adj.add(0, "siteRef", "site-1");
112 adj.add(1, "siteRef", "site-1");
113
114 adj.remove(0);
115
116 assert!(adj.targets_from(0, None).is_empty());
117 let sources = adj.sources_to("site-1", None);
119 assert_eq!(sources, vec![1]);
120 }
121
122 #[test]
123 fn targets_from_with_type_filter() {
124 let mut adj = RefAdjacency::new();
125 adj.add(0, "siteRef", "site-1");
126 adj.add(0, "equipRef", "equip-1");
127
128 let site_targets = adj.targets_from(0, Some("siteRef"));
129 assert_eq!(site_targets, vec!["site-1".to_string()]);
130
131 let equip_targets = adj.targets_from(0, Some("equipRef"));
132 assert_eq!(equip_targets, vec!["equip-1".to_string()]);
133
134 let none_targets = adj.targets_from(0, Some("spaceRef"));
135 assert!(none_targets.is_empty());
136 }
137
138 #[test]
139 fn targets_from_without_type_filter() {
140 let mut adj = RefAdjacency::new();
141 adj.add(0, "siteRef", "site-1");
142 adj.add(0, "equipRef", "equip-1");
143
144 let all = adj.targets_from(0, None);
145 assert_eq!(all.len(), 2);
146 }
147
148 #[test]
149 fn sources_to_with_type_filter() {
150 let mut adj = RefAdjacency::new();
151 adj.add(0, "siteRef", "site-1");
152 adj.add(1, "equipRef", "site-1");
153
154 let site_sources = adj.sources_to("site-1", Some("siteRef"));
155 assert_eq!(site_sources, vec![0]);
156
157 let equip_sources = adj.sources_to("site-1", Some("equipRef"));
158 assert_eq!(equip_sources, vec![1]);
159 }
160
161 #[test]
162 fn sources_to_without_type_filter() {
163 let mut adj = RefAdjacency::new();
164 adj.add(0, "siteRef", "site-1");
165 adj.add(1, "equipRef", "site-1");
166
167 let all = adj.sources_to("site-1", None);
168 assert_eq!(all.len(), 2);
169 }
170
171 #[test]
172 fn targets_from_nonexistent_entity() {
173 let adj = RefAdjacency::new();
174 assert!(adj.targets_from(999, None).is_empty());
175 }
176
177 #[test]
178 fn sources_to_nonexistent_target() {
179 let adj = RefAdjacency::new();
180 assert!(adj.sources_to("nonexistent", None).is_empty());
181 }
182
183 #[test]
184 fn remove_nonexistent_entity_is_noop() {
185 let mut adj = RefAdjacency::new();
186 adj.remove(999);
188 }
189
190 #[test]
191 fn remove_cleans_up_reverse_entry() {
192 let mut adj = RefAdjacency::new();
193 adj.add(0, "siteRef", "site-1");
194
195 adj.remove(0);
196
197 assert!(adj.sources_to("site-1", None).is_empty());
199 }
200}