use std::collections::{HashMap, HashSet};
use crate::error::CapabilityError;
#[derive(Debug, Clone, Default)]
pub struct CapabilityIdIndex {
canonical: HashSet<String>,
aliases: HashMap<String, String>,
}
impl CapabilityIdIndex {
pub fn new() -> Self {
Self::default()
}
pub fn insert(
&mut self,
canonical: impl Into<String>,
aliases: &[&str],
) -> Result<(), CapabilityError> {
let canonical = canonical.into();
if self.contains(&canonical) {
return Err(CapabilityError::Duplicate { id: canonical });
}
for alias in aliases {
if self.contains(alias) {
return Err(CapabilityError::Duplicate {
id: (*alias).to_string(),
});
}
}
for alias in aliases {
self.aliases.insert((*alias).to_string(), canonical.clone());
}
self.canonical.insert(canonical);
Ok(())
}
pub fn insert_or_replace(&mut self, canonical: impl Into<String>, aliases: &[&str]) {
let canonical = canonical.into();
self.remove(&canonical);
for alias in aliases {
self.aliases.insert((*alias).to_string(), canonical.clone());
}
self.canonical.insert(canonical);
}
pub fn canonical_of<'a>(&'a self, id: &'a str) -> Option<&'a str> {
if self.canonical.contains(id) {
Some(id)
} else {
self.aliases
.get(id)
.filter(|canonical| self.canonical.contains(*canonical))
.map(String::as_str)
}
}
pub fn contains(&self, id: &str) -> bool {
self.canonical.contains(id) || self.aliases.contains_key(id)
}
pub fn remove(&mut self, id: &str) -> Option<String> {
let canonical = self.canonical_of(id)?.to_string();
self.canonical.remove(&canonical);
self.aliases.retain(|_, target| *target != canonical);
Some(canonical)
}
pub fn canonical_ids(&self) -> impl Iterator<Item = &str> {
self.canonical.iter().map(String::as_str)
}
}
#[derive(Debug, Clone, Default)]
pub struct ActivationSet {
seen: HashSet<String>,
}
impl ActivationSet {
pub fn new() -> Self {
Self::default()
}
pub fn activate(&mut self, canonical_id: impl Into<String>) -> Result<(), CapabilityError> {
let canonical_id = canonical_id.into();
if !self.seen.insert(canonical_id.clone()) {
return Err(CapabilityError::Duplicate { id: canonical_id });
}
Ok(())
}
pub fn contains(&self, canonical_id: &str) -> bool {
self.seen.contains(canonical_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn index_rejects_duplicate_canonical_and_alias() {
let mut index = CapabilityIdIndex::new();
index.insert("bashkit_shell", &["virtual_bash"]).unwrap();
let err = index.insert("bashkit_shell", &[]).unwrap_err();
assert!(err.is_duplicate());
let err = index.insert("other", &["virtual_bash"]).unwrap_err();
assert!(err.is_duplicate());
let err = index.insert("virtual_bash", &[]).unwrap_err();
assert!(err.is_duplicate());
}
#[test]
fn index_resolves_aliases_to_canonical() {
let mut index = CapabilityIdIndex::new();
index.insert("bashkit_shell", &["virtual_bash"]).unwrap();
assert_eq!(index.canonical_of("bashkit_shell"), Some("bashkit_shell"));
assert_eq!(index.canonical_of("virtual_bash"), Some("bashkit_shell"));
assert_eq!(index.canonical_of("unknown"), None);
}
#[test]
fn replace_and_remove() {
let mut index = CapabilityIdIndex::new();
index.insert("cap", &["old_cap"]).unwrap();
index.insert_or_replace("cap", &["older_cap"]);
assert_eq!(index.canonical_of("older_cap"), Some("cap"));
assert_eq!(index.canonical_of("old_cap"), None);
assert_eq!(index.remove("older_cap"), Some("cap".to_string()));
assert!(!index.contains("cap"));
assert!(!index.contains("older_cap"));
}
#[test]
fn activation_set_rejects_second_activation() {
let mut set = ActivationSet::new();
set.activate("current_time").unwrap();
let err = set.activate("current_time").unwrap_err();
assert_eq!(err.id(), "current_time");
assert!(err.is_duplicate());
}
}