1use 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 pub fn sources_for(&self, ref_tag: &str, target_ref_val: &str) -> Vec<usize> {
105 match self.reverse.get(target_ref_val) {
106 Some(edges) => edges
107 .iter()
108 .filter(|(rt, _)| rt == ref_tag)
109 .map(|(_, eid)| *eid)
110 .collect(),
111 None => Vec::new(),
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn add_forward_and_reverse_edges() {
122 let mut adj = RefAdjacency::new();
123 adj.add(0, "siteRef", "site-1");
124 adj.add(0, "equipRef", "equip-1");
125
126 let targets = adj.targets_from(0, None);
127 assert_eq!(targets.len(), 2);
128 assert!(targets.contains(&"site-1".to_string()));
129 assert!(targets.contains(&"equip-1".to_string()));
130
131 let sources = adj.sources_to("site-1", None);
132 assert_eq!(sources, vec![0]);
133 }
134
135 #[test]
136 fn remove_entity_edges() {
137 let mut adj = RefAdjacency::new();
138 adj.add(0, "siteRef", "site-1");
139 adj.add(1, "siteRef", "site-1");
140
141 adj.remove(0);
142
143 assert!(adj.targets_from(0, None).is_empty());
144 let sources = adj.sources_to("site-1", None);
146 assert_eq!(sources, vec![1]);
147 }
148
149 #[test]
150 fn targets_from_with_type_filter() {
151 let mut adj = RefAdjacency::new();
152 adj.add(0, "siteRef", "site-1");
153 adj.add(0, "equipRef", "equip-1");
154
155 let site_targets = adj.targets_from(0, Some("siteRef"));
156 assert_eq!(site_targets, vec!["site-1".to_string()]);
157
158 let equip_targets = adj.targets_from(0, Some("equipRef"));
159 assert_eq!(equip_targets, vec!["equip-1".to_string()]);
160
161 let none_targets = adj.targets_from(0, Some("spaceRef"));
162 assert!(none_targets.is_empty());
163 }
164
165 #[test]
166 fn targets_from_without_type_filter() {
167 let mut adj = RefAdjacency::new();
168 adj.add(0, "siteRef", "site-1");
169 adj.add(0, "equipRef", "equip-1");
170
171 let all = adj.targets_from(0, None);
172 assert_eq!(all.len(), 2);
173 }
174
175 #[test]
176 fn sources_to_with_type_filter() {
177 let mut adj = RefAdjacency::new();
178 adj.add(0, "siteRef", "site-1");
179 adj.add(1, "equipRef", "site-1");
180
181 let site_sources = adj.sources_to("site-1", Some("siteRef"));
182 assert_eq!(site_sources, vec![0]);
183
184 let equip_sources = adj.sources_to("site-1", Some("equipRef"));
185 assert_eq!(equip_sources, vec![1]);
186 }
187
188 #[test]
189 fn sources_to_without_type_filter() {
190 let mut adj = RefAdjacency::new();
191 adj.add(0, "siteRef", "site-1");
192 adj.add(1, "equipRef", "site-1");
193
194 let all = adj.sources_to("site-1", None);
195 assert_eq!(all.len(), 2);
196 }
197
198 #[test]
199 fn targets_from_nonexistent_entity() {
200 let adj = RefAdjacency::new();
201 assert!(adj.targets_from(999, None).is_empty());
202 }
203
204 #[test]
205 fn sources_to_nonexistent_target() {
206 let adj = RefAdjacency::new();
207 assert!(adj.sources_to("nonexistent", None).is_empty());
208 }
209
210 #[test]
211 fn remove_nonexistent_entity_is_noop() {
212 let mut adj = RefAdjacency::new();
213 adj.remove(999);
215 }
216
217 #[test]
218 fn remove_cleans_up_reverse_entry() {
219 let mut adj = RefAdjacency::new();
220 adj.add(0, "siteRef", "site-1");
221
222 adj.remove(0);
223
224 assert!(adj.sources_to("site-1", None).is_empty());
226 }
227}