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
89impl RefAdjacency {
90 pub fn forward_raw(&self) -> &HashMap<usize, SmallVec<[(String, String); 4]>> {
92 &self.forward
93 }
94
95 pub fn reverse_raw(&self) -> &HashMap<String, SmallVec<[(String, usize); 4]>> {
97 &self.reverse
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn add_forward_and_reverse_edges() {
107 let mut adj = RefAdjacency::new();
108 adj.add(0, "siteRef", "site-1");
109 adj.add(0, "equipRef", "equip-1");
110
111 let targets = adj.targets_from(0, None);
112 assert_eq!(targets.len(), 2);
113 assert!(targets.contains(&"site-1".to_string()));
114 assert!(targets.contains(&"equip-1".to_string()));
115
116 let sources = adj.sources_to("site-1", None);
117 assert_eq!(sources, vec![0]);
118 }
119
120 #[test]
121 fn remove_entity_edges() {
122 let mut adj = RefAdjacency::new();
123 adj.add(0, "siteRef", "site-1");
124 adj.add(1, "siteRef", "site-1");
125
126 adj.remove(0);
127
128 assert!(adj.targets_from(0, None).is_empty());
129 let sources = adj.sources_to("site-1", None);
131 assert_eq!(sources, vec![1]);
132 }
133
134 #[test]
135 fn targets_from_with_type_filter() {
136 let mut adj = RefAdjacency::new();
137 adj.add(0, "siteRef", "site-1");
138 adj.add(0, "equipRef", "equip-1");
139
140 let site_targets = adj.targets_from(0, Some("siteRef"));
141 assert_eq!(site_targets, vec!["site-1".to_string()]);
142
143 let equip_targets = adj.targets_from(0, Some("equipRef"));
144 assert_eq!(equip_targets, vec!["equip-1".to_string()]);
145
146 let none_targets = adj.targets_from(0, Some("spaceRef"));
147 assert!(none_targets.is_empty());
148 }
149
150 #[test]
151 fn targets_from_without_type_filter() {
152 let mut adj = RefAdjacency::new();
153 adj.add(0, "siteRef", "site-1");
154 adj.add(0, "equipRef", "equip-1");
155
156 let all = adj.targets_from(0, None);
157 assert_eq!(all.len(), 2);
158 }
159
160 #[test]
161 fn sources_to_with_type_filter() {
162 let mut adj = RefAdjacency::new();
163 adj.add(0, "siteRef", "site-1");
164 adj.add(1, "equipRef", "site-1");
165
166 let site_sources = adj.sources_to("site-1", Some("siteRef"));
167 assert_eq!(site_sources, vec![0]);
168
169 let equip_sources = adj.sources_to("site-1", Some("equipRef"));
170 assert_eq!(equip_sources, vec![1]);
171 }
172
173 #[test]
174 fn sources_to_without_type_filter() {
175 let mut adj = RefAdjacency::new();
176 adj.add(0, "siteRef", "site-1");
177 adj.add(1, "equipRef", "site-1");
178
179 let all = adj.sources_to("site-1", None);
180 assert_eq!(all.len(), 2);
181 }
182
183 #[test]
184 fn targets_from_nonexistent_entity() {
185 let adj = RefAdjacency::new();
186 assert!(adj.targets_from(999, None).is_empty());
187 }
188
189 #[test]
190 fn sources_to_nonexistent_target() {
191 let adj = RefAdjacency::new();
192 assert!(adj.sources_to("nonexistent", None).is_empty());
193 }
194
195 #[test]
196 fn remove_nonexistent_entity_is_noop() {
197 let mut adj = RefAdjacency::new();
198 adj.remove(999);
200 }
201
202 #[test]
203 fn remove_cleans_up_reverse_entry() {
204 let mut adj = RefAdjacency::new();
205 adj.add(0, "siteRef", "site-1");
206
207 adj.remove(0);
208
209 assert!(adj.sources_to("site-1", None).is_empty());
211 }
212}