use ahash::AHashMap;
use rialo_feature_management_enforcement_interface::state::EnforcementState;
pub type Slot = u64;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ActiveFeatures {
#[allow(
clippy::disallowed_types,
reason = "the only persisted AHashMap state in this file; every access is either a keyed lookup (is_active, snapshot_slot) or the iter() view, whose only two consumers do not depend on order — an RPC handler that sorts its collected output by name, and a per-block readiness check whose only output is a local diagnostic naming the first unsupported feature"
)]
active: AHashMap<String, Slot>,
}
impl ActiveFeatures {
pub fn new() -> Self {
Self::default()
}
#[allow(
clippy::disallowed_types,
reason = "transient construction site: builds the map from EnforcementState::entries() and immediately hands it to from_map, which stores it as ActiveFeatures::active; see that field's reason for why order does not matter"
)]
pub fn from_enforcement_state(state: &EnforcementState, snapshot_slot: Slot) -> Self {
let active: AHashMap<String, Slot> = state
.entries()
.iter()
.map(|name| (name.clone(), snapshot_slot))
.collect();
Self::from_map(active)
}
#[allow(
clippy::disallowed_types,
reason = "receives the already-built map and stores it verbatim as ActiveFeatures::active; see that field's reason for why order does not matter"
)]
pub(crate) fn from_map(active: AHashMap<String, Slot>) -> Self {
Self { active }
}
#[cfg(any(test, feature = "dev-context-only-utils"))]
#[allow(
clippy::disallowed_types,
reason = "test/dev-only construction site: every override shares snapshot slot 0, so the collect() order is discarded before the map reaches from_map"
)]
pub fn with_overrides(features: &[&str]) -> Self {
let active: AHashMap<String, Slot> = features
.iter()
.map(|name| ((*name).to_string(), 0))
.collect();
Self::from_map(active)
}
pub fn is_active(&self, feature: &str) -> bool {
self.active.contains_key(feature)
}
pub fn snapshot_slot(&self, feature: &str) -> Option<Slot> {
self.active.get(feature).copied()
}
pub fn len(&self) -> usize {
self.active.len()
}
pub fn is_empty(&self) -> bool {
self.active.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, Slot)> + '_ {
self.active.iter().map(|(k, v)| (k.as_str(), *v))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(
clippy::disallowed_types,
reason = "test helper that builds a fixture map by inserting keys one at a time before handing it to from_map; order is irrelevant to the fixture, and tests that need a specific order sort explicitly (test_iter_covers_all_entries)"
)]
fn make(entries: &[(&str, Slot)]) -> ActiveFeatures {
let mut m = AHashMap::new();
for (k, v) in entries {
m.insert((*k).to_string(), *v);
}
ActiveFeatures::from_map(m)
}
#[test]
fn test_empty_is_empty() {
let a = ActiveFeatures::new();
assert!(a.is_empty());
assert_eq!(a.len(), 0);
assert!(!a.is_active("anything"));
assert_eq!(a.snapshot_slot("anything"), None);
}
#[test]
fn test_is_active_reflects_membership() {
let a = make(&[("foo", 42), ("bar", 100)]);
assert!(a.is_active("foo"));
assert!(a.is_active("bar"));
assert!(!a.is_active("baz"));
}
#[test]
fn test_snapshot_slot_returns_recorded_value() {
let a = make(&[("foo", 42)]);
assert_eq!(a.snapshot_slot("foo"), Some(42));
assert_eq!(a.snapshot_slot("missing"), None);
}
#[test]
fn test_iter_covers_all_entries() {
let a = make(&[("foo", 1), ("bar", 2)]);
let mut seen: Vec<(&str, Slot)> = a.iter().collect();
seen.sort_by_key(|(k, _)| *k);
assert_eq!(seen, vec![("bar", 2), ("foo", 1)]);
}
#[test]
fn test_with_overrides_marks_listed_features_active() {
let a = ActiveFeatures::with_overrides(&["foo", "bar"]);
assert!(a.is_active("foo"));
assert!(a.is_active("bar"));
assert!(!a.is_active("baz"));
assert_eq!(a.snapshot_slot("foo"), Some(0));
}
#[test]
fn test_with_overrides_empty_yields_empty() {
let a = ActiveFeatures::with_overrides(&[]);
assert!(a.is_empty());
}
}