use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Feature {
Dashboard,
Run,
Exec,
Agents,
Inventory,
Compliance,
Activity,
Events,
Audit,
Logs,
Collect,
Analytics,
Jobs,
Schedules,
Views,
Notifications,
Rollout,
Apps,
Groups,
Config,
Jetstream,
Accounts,
Settings,
}
impl Feature {
pub fn as_str(self) -> &'static str {
match self {
Feature::Dashboard => "dashboard",
Feature::Run => "run",
Feature::Exec => "exec",
Feature::Agents => "agents",
Feature::Inventory => "inventory",
Feature::Compliance => "compliance",
Feature::Activity => "activity",
Feature::Events => "events",
Feature::Audit => "audit",
Feature::Logs => "logs",
Feature::Collect => "collect",
Feature::Analytics => "analytics",
Feature::Jobs => "jobs",
Feature::Schedules => "schedules",
Feature::Views => "views",
Feature::Notifications => "notifications",
Feature::Rollout => "rollout",
Feature::Apps => "apps",
Feature::Groups => "groups",
Feature::Config => "config",
Feature::Jetstream => "jetstream",
Feature::Accounts => "accounts",
Feature::Settings => "settings",
}
}
pub fn parse(s: &str) -> Option<Feature> {
Some(match s {
"dashboard" => Feature::Dashboard,
"run" => Feature::Run,
"exec" => Feature::Exec,
"agents" => Feature::Agents,
"inventory" => Feature::Inventory,
"compliance" => Feature::Compliance,
"activity" => Feature::Activity,
"events" => Feature::Events,
"audit" => Feature::Audit,
"logs" => Feature::Logs,
"collect" => Feature::Collect,
"analytics" => Feature::Analytics,
"jobs" => Feature::Jobs,
"schedules" => Feature::Schedules,
"views" => Feature::Views,
"notifications" => Feature::Notifications,
"rollout" => Feature::Rollout,
"apps" => Feature::Apps,
"groups" => Feature::Groups,
"config" => Feature::Config,
"jetstream" => Feature::Jetstream,
"accounts" => Feature::Accounts,
"settings" => Feature::Settings,
_ => return None,
})
}
pub fn canonicalize(keys: &[String]) -> Result<Vec<String>, String> {
let mut set: Vec<Feature> = Vec::new();
for k in keys {
let f = Feature::parse(k).ok_or_else(|| k.clone())?;
if !set.contains(&f) {
set.push(f);
}
}
Ok(Feature::ALL
.iter()
.filter(|f| set.contains(f))
.map(|f| f.as_str().to_string())
.collect())
}
pub const ALL: [Feature; 23] = [
Feature::Dashboard,
Feature::Run,
Feature::Exec,
Feature::Agents,
Feature::Inventory,
Feature::Compliance,
Feature::Activity,
Feature::Events,
Feature::Audit,
Feature::Logs,
Feature::Collect,
Feature::Analytics,
Feature::Jobs,
Feature::Schedules,
Feature::Views,
Feature::Notifications,
Feature::Rollout,
Feature::Apps,
Feature::Groups,
Feature::Config,
Feature::Jetstream,
Feature::Accounts,
Feature::Settings,
];
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_roundtrips_for_every_variant() {
for f in Feature::ALL {
assert_eq!(Feature::parse(f.as_str()), Some(f), "roundtrip {f:?}");
}
}
#[test]
fn all_covers_the_enum() {
assert_eq!(Feature::ALL.len(), 23);
let mut keys: Vec<&str> = Feature::ALL.iter().map(|f| f.as_str()).collect();
keys.sort_unstable();
keys.dedup();
assert_eq!(keys.len(), Feature::ALL.len(), "duplicate feature key");
}
#[test]
fn serde_uses_lowercase_key() {
assert_eq!(
serde_json::to_string(&Feature::Compliance).unwrap(),
"\"compliance\""
);
assert_eq!(
serde_json::from_str::<Feature>("\"jetstream\"").unwrap(),
Feature::Jetstream
);
}
#[test]
fn unknown_key_is_none() {
assert_eq!(Feature::parse("nope"), None);
assert_eq!(Feature::parse("Compliance"), None); }
#[test]
fn canonicalize_validates_dedupes_orders() {
assert_eq!(
Feature::canonicalize(&["bogus".into()]),
Err("bogus".to_string())
);
assert_eq!(
Feature::canonicalize(&["compliance".into(), "inventory".into(), "compliance".into()]),
Ok(vec!["inventory".to_string(), "compliance".to_string()])
);
assert_eq!(Feature::canonicalize(&[]), Ok(vec![]));
}
}