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 Remote,
57}
58
59impl Feature {
60 pub fn as_str(self) -> &'static str {
63 match self {
64 Feature::Dashboard => "dashboard",
65 Feature::Run => "run",
66 Feature::Exec => "exec",
67 Feature::Agents => "agents",
68 Feature::Inventory => "inventory",
69 Feature::Compliance => "compliance",
70 Feature::Activity => "activity",
71 Feature::Events => "events",
72 Feature::Audit => "audit",
73 Feature::Logs => "logs",
74 Feature::Collect => "collect",
75 Feature::Analytics => "analytics",
76 Feature::Jobs => "jobs",
77 Feature::Schedules => "schedules",
78 Feature::Views => "views",
79 Feature::Notifications => "notifications",
80 Feature::Rollout => "rollout",
81 Feature::Apps => "apps",
82 Feature::Groups => "groups",
83 Feature::Config => "config",
84 Feature::Jetstream => "jetstream",
85 Feature::Accounts => "accounts",
86 Feature::Settings => "settings",
87 Feature::Remote => "remote",
88 }
89 }
90
91 pub fn parse(s: &str) -> Option<Feature> {
95 Some(match s {
96 "dashboard" => Feature::Dashboard,
97 "run" => Feature::Run,
98 "exec" => Feature::Exec,
99 "agents" => Feature::Agents,
100 "inventory" => Feature::Inventory,
101 "compliance" => Feature::Compliance,
102 "activity" => Feature::Activity,
103 "events" => Feature::Events,
104 "audit" => Feature::Audit,
105 "logs" => Feature::Logs,
106 "collect" => Feature::Collect,
107 "analytics" => Feature::Analytics,
108 "jobs" => Feature::Jobs,
109 "schedules" => Feature::Schedules,
110 "views" => Feature::Views,
111 "notifications" => Feature::Notifications,
112 "rollout" => Feature::Rollout,
113 "apps" => Feature::Apps,
114 "groups" => Feature::Groups,
115 "config" => Feature::Config,
116 "jetstream" => Feature::Jetstream,
117 "accounts" => Feature::Accounts,
118 "settings" => Feature::Settings,
119 "remote" => Feature::Remote,
120 _ => return None,
121 })
122 }
123
124 pub fn canonicalize(keys: &[String]) -> Result<Vec<String>, String> {
130 let mut set: Vec<Feature> = Vec::new();
131 for k in keys {
132 let f = Feature::parse(k).ok_or_else(|| k.clone())?;
133 if !set.contains(&f) {
134 set.push(f);
135 }
136 }
137 Ok(Feature::ALL
138 .iter()
139 .filter(|f| set.contains(f))
140 .map(|f| f.as_str().to_string())
141 .collect())
142 }
143
144 pub const ALL: [Feature; 24] = [
148 Feature::Dashboard,
149 Feature::Run,
150 Feature::Exec,
151 Feature::Agents,
152 Feature::Inventory,
153 Feature::Compliance,
154 Feature::Activity,
155 Feature::Events,
156 Feature::Audit,
157 Feature::Logs,
158 Feature::Collect,
159 Feature::Analytics,
160 Feature::Jobs,
161 Feature::Schedules,
162 Feature::Views,
163 Feature::Notifications,
164 Feature::Rollout,
165 Feature::Apps,
166 Feature::Groups,
167 Feature::Config,
168 Feature::Jetstream,
169 Feature::Accounts,
170 Feature::Settings,
171 Feature::Remote,
172 ];
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn key_roundtrips_for_every_variant() {
181 for f in Feature::ALL {
182 assert_eq!(Feature::parse(f.as_str()), Some(f), "roundtrip {f:?}");
183 }
184 }
185
186 #[test]
187 fn all_covers_the_enum() {
188 assert_eq!(Feature::ALL.len(), 24);
193 let mut keys: Vec<&str> = Feature::ALL.iter().map(|f| f.as_str()).collect();
195 keys.sort_unstable();
196 keys.dedup();
197 assert_eq!(keys.len(), Feature::ALL.len(), "duplicate feature key");
198 }
199
200 #[test]
201 fn serde_uses_lowercase_key() {
202 assert_eq!(
203 serde_json::to_string(&Feature::Compliance).unwrap(),
204 "\"compliance\""
205 );
206 assert_eq!(
207 serde_json::from_str::<Feature>("\"jetstream\"").unwrap(),
208 Feature::Jetstream
209 );
210 }
211
212 #[test]
213 fn unknown_key_is_none() {
214 assert_eq!(Feature::parse("nope"), None);
215 assert_eq!(Feature::parse("Compliance"), None); }
217
218 #[test]
219 fn canonicalize_validates_dedupes_orders() {
220 assert_eq!(
222 Feature::canonicalize(&["bogus".into()]),
223 Err("bogus".to_string())
224 );
225 assert_eq!(
227 Feature::canonicalize(&["compliance".into(), "inventory".into(), "compliance".into()]),
228 Ok(vec!["inventory".to_string(), "compliance".to_string()])
229 );
230 assert_eq!(Feature::canonicalize(&[]), Ok(vec![]));
232 }
233}