1use serde::{Deserialize, Serialize};
21
22#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27pub enum Feature {
28 Dashboard,
29 Run,
30 Exec,
31 Agents,
32 Inventory,
33 Compliance,
34 Activity,
35 Events,
36 Audit,
37 Logs,
38 Collect,
39 Analytics,
40 Jobs,
41 Schedules,
42 Views,
43 Notifications,
44 Rollout,
45 Apps,
46 Groups,
47 Config,
48 Jetstream,
49 Accounts,
50 Settings,
51}
52
53impl Feature {
54 pub fn as_str(self) -> &'static str {
57 match self {
58 Feature::Dashboard => "dashboard",
59 Feature::Run => "run",
60 Feature::Exec => "exec",
61 Feature::Agents => "agents",
62 Feature::Inventory => "inventory",
63 Feature::Compliance => "compliance",
64 Feature::Activity => "activity",
65 Feature::Events => "events",
66 Feature::Audit => "audit",
67 Feature::Logs => "logs",
68 Feature::Collect => "collect",
69 Feature::Analytics => "analytics",
70 Feature::Jobs => "jobs",
71 Feature::Schedules => "schedules",
72 Feature::Views => "views",
73 Feature::Notifications => "notifications",
74 Feature::Rollout => "rollout",
75 Feature::Apps => "apps",
76 Feature::Groups => "groups",
77 Feature::Config => "config",
78 Feature::Jetstream => "jetstream",
79 Feature::Accounts => "accounts",
80 Feature::Settings => "settings",
81 }
82 }
83
84 pub fn parse(s: &str) -> Option<Feature> {
88 Some(match s {
89 "dashboard" => Feature::Dashboard,
90 "run" => Feature::Run,
91 "exec" => Feature::Exec,
92 "agents" => Feature::Agents,
93 "inventory" => Feature::Inventory,
94 "compliance" => Feature::Compliance,
95 "activity" => Feature::Activity,
96 "events" => Feature::Events,
97 "audit" => Feature::Audit,
98 "logs" => Feature::Logs,
99 "collect" => Feature::Collect,
100 "analytics" => Feature::Analytics,
101 "jobs" => Feature::Jobs,
102 "schedules" => Feature::Schedules,
103 "views" => Feature::Views,
104 "notifications" => Feature::Notifications,
105 "rollout" => Feature::Rollout,
106 "apps" => Feature::Apps,
107 "groups" => Feature::Groups,
108 "config" => Feature::Config,
109 "jetstream" => Feature::Jetstream,
110 "accounts" => Feature::Accounts,
111 "settings" => Feature::Settings,
112 _ => return None,
113 })
114 }
115
116 pub fn canonicalize(keys: &[String]) -> Result<Vec<String>, String> {
122 let mut set: Vec<Feature> = Vec::new();
123 for k in keys {
124 let f = Feature::parse(k).ok_or_else(|| k.clone())?;
125 if !set.contains(&f) {
126 set.push(f);
127 }
128 }
129 Ok(Feature::ALL
130 .iter()
131 .filter(|f| set.contains(f))
132 .map(|f| f.as_str().to_string())
133 .collect())
134 }
135
136 pub const ALL: [Feature; 23] = [
140 Feature::Dashboard,
141 Feature::Run,
142 Feature::Exec,
143 Feature::Agents,
144 Feature::Inventory,
145 Feature::Compliance,
146 Feature::Activity,
147 Feature::Events,
148 Feature::Audit,
149 Feature::Logs,
150 Feature::Collect,
151 Feature::Analytics,
152 Feature::Jobs,
153 Feature::Schedules,
154 Feature::Views,
155 Feature::Notifications,
156 Feature::Rollout,
157 Feature::Apps,
158 Feature::Groups,
159 Feature::Config,
160 Feature::Jetstream,
161 Feature::Accounts,
162 Feature::Settings,
163 ];
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn key_roundtrips_for_every_variant() {
172 for f in Feature::ALL {
173 assert_eq!(Feature::parse(f.as_str()), Some(f), "roundtrip {f:?}");
174 }
175 }
176
177 #[test]
178 fn all_covers_the_enum() {
179 assert_eq!(Feature::ALL.len(), 23);
184 let mut keys: Vec<&str> = Feature::ALL.iter().map(|f| f.as_str()).collect();
186 keys.sort_unstable();
187 keys.dedup();
188 assert_eq!(keys.len(), Feature::ALL.len(), "duplicate feature key");
189 }
190
191 #[test]
192 fn serde_uses_lowercase_key() {
193 assert_eq!(
194 serde_json::to_string(&Feature::Compliance).unwrap(),
195 "\"compliance\""
196 );
197 assert_eq!(
198 serde_json::from_str::<Feature>("\"jetstream\"").unwrap(),
199 Feature::Jetstream
200 );
201 }
202
203 #[test]
204 fn unknown_key_is_none() {
205 assert_eq!(Feature::parse("nope"), None);
206 assert_eq!(Feature::parse("Compliance"), None); }
208
209 #[test]
210 fn canonicalize_validates_dedupes_orders() {
211 assert_eq!(
213 Feature::canonicalize(&["bogus".into()]),
214 Err("bogus".to_string())
215 );
216 assert_eq!(
218 Feature::canonicalize(&["compliance".into(), "inventory".into(), "compliance".into()]),
219 Ok(vec!["inventory".to_string(), "compliance".to_string()])
220 );
221 assert_eq!(Feature::canonicalize(&[]), Ok(vec![]));
223 }
224}