everruns_capability/
registry.rs1use std::collections::{HashMap, HashSet};
5
6use crate::error::CapabilityError;
7
8#[derive(Debug, Clone, Default)]
17pub struct CapabilityIdIndex {
18 canonical: HashSet<String>,
19 aliases: HashMap<String, String>,
21}
22
23impl CapabilityIdIndex {
24 pub fn new() -> Self {
26 Self::default()
27 }
28
29 pub fn insert(
34 &mut self,
35 canonical: impl Into<String>,
36 aliases: &[&str],
37 ) -> Result<(), CapabilityError> {
38 let canonical = canonical.into();
39 if self.contains(&canonical) {
40 return Err(CapabilityError::Duplicate { id: canonical });
41 }
42 for alias in aliases {
43 if self.contains(alias) {
44 return Err(CapabilityError::Duplicate {
45 id: (*alias).to_string(),
46 });
47 }
48 }
49 for alias in aliases {
50 self.aliases.insert((*alias).to_string(), canonical.clone());
51 }
52 self.canonical.insert(canonical);
53 Ok(())
54 }
55
56 pub fn insert_or_replace(&mut self, canonical: impl Into<String>, aliases: &[&str]) {
59 let canonical = canonical.into();
60 self.remove(&canonical);
61 for alias in aliases {
62 self.aliases.insert((*alias).to_string(), canonical.clone());
63 }
64 self.canonical.insert(canonical);
65 }
66
67 pub fn canonical_of<'a>(&'a self, id: &'a str) -> Option<&'a str> {
69 if self.canonical.contains(id) {
70 Some(id)
71 } else {
72 self.aliases
73 .get(id)
74 .filter(|canonical| self.canonical.contains(*canonical))
75 .map(String::as_str)
76 }
77 }
78
79 pub fn contains(&self, id: &str) -> bool {
81 self.canonical.contains(id) || self.aliases.contains_key(id)
82 }
83
84 pub fn remove(&mut self, id: &str) -> Option<String> {
88 let canonical = self.canonical_of(id)?.to_string();
89 self.canonical.remove(&canonical);
90 self.aliases.retain(|_, target| *target != canonical);
91 Some(canonical)
92 }
93
94 pub fn canonical_ids(&self) -> impl Iterator<Item = &str> {
96 self.canonical.iter().map(String::as_str)
97 }
98}
99
100#[derive(Debug, Clone, Default)]
107pub struct ActivationSet {
108 seen: HashSet<String>,
109}
110
111impl ActivationSet {
112 pub fn new() -> Self {
114 Self::default()
115 }
116
117 pub fn activate(&mut self, canonical_id: impl Into<String>) -> Result<(), CapabilityError> {
119 let canonical_id = canonical_id.into();
120 if !self.seen.insert(canonical_id.clone()) {
121 return Err(CapabilityError::Duplicate { id: canonical_id });
122 }
123 Ok(())
124 }
125
126 pub fn contains(&self, canonical_id: &str) -> bool {
128 self.seen.contains(canonical_id)
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn index_rejects_duplicate_canonical_and_alias() {
138 let mut index = CapabilityIdIndex::new();
139 index.insert("bashkit_shell", &["virtual_bash"]).unwrap();
140
141 let err = index.insert("bashkit_shell", &[]).unwrap_err();
142 assert!(err.is_duplicate());
143 let err = index.insert("other", &["virtual_bash"]).unwrap_err();
144 assert!(err.is_duplicate());
145 let err = index.insert("virtual_bash", &[]).unwrap_err();
147 assert!(err.is_duplicate());
148 let err = index
149 .insert("other", &["fresh_alias", "bashkit_shell"])
150 .unwrap_err();
151 assert_eq!(err.id(), "bashkit_shell");
152 assert!(err.is_duplicate());
153 assert!(!index.contains("other"));
154 assert!(!index.contains("fresh_alias"));
155 assert_eq!(index.canonical_of("virtual_bash"), Some("bashkit_shell"));
156 }
157
158 #[test]
159 fn index_resolves_aliases_to_canonical() {
160 let mut index = CapabilityIdIndex::new();
161 index.insert("bashkit_shell", &["virtual_bash"]).unwrap();
162 assert_eq!(index.canonical_of("bashkit_shell"), Some("bashkit_shell"));
163 assert_eq!(index.canonical_of("virtual_bash"), Some("bashkit_shell"));
164 assert_eq!(index.canonical_of("unknown"), None);
165 }
166
167 #[test]
168 fn replace_and_remove() {
169 let mut index = CapabilityIdIndex::new();
170 index.insert("cap", &["old_cap"]).unwrap();
171 index.insert("other", &["other_alias"]).unwrap();
172 index.insert_or_replace("cap", &["older_cap"]);
173 assert_eq!(index.canonical_of("older_cap"), Some("cap"));
174 assert_eq!(index.canonical_of("old_cap"), None);
175
176 assert_eq!(index.remove("older_cap"), Some("cap".to_string()));
177 assert!(!index.contains("cap"));
178 assert!(!index.contains("older_cap"));
179 assert_eq!(index.canonical_of("other_alias"), Some("other"));
180 assert_eq!(index.canonical_ids().collect::<Vec<_>>(), ["other"]);
181 assert_eq!(index.remove("missing"), None);
182 }
183
184 #[test]
185 fn activation_set_rejects_second_activation() {
186 let mut set = ActivationSet::new();
187 set.activate("current_time").unwrap();
188 let err = set.activate("current_time").unwrap_err();
189 assert_eq!(err.id(), "current_time");
190 assert!(err.is_duplicate());
191 assert!(set.contains("current_time"));
192 assert!(!set.contains("web_fetch"));
193 set.activate("web_fetch").unwrap();
194 assert!(set.contains("web_fetch"));
195 }
196}