uptrakit_wire/
service_profile.rs1use std::collections::BTreeSet;
9
10use crate::Capability;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum ServiceProfile {
25 UpdateTracker,
27 Agent,
29 Scheduler,
31 Unknown,
33}
34
35impl ServiceProfile {
36 pub fn from_capabilities(caps: &BTreeSet<Capability>) -> Self {
40 if caps.contains(&Capability::UpdateTracking) {
41 Self::UpdateTracker
42 } else if caps.contains(&Capability::Scheduler) {
43 Self::Scheduler
44 } else if caps.contains(&Capability::SoftwareDiscovery) {
45 Self::Agent
46 } else {
47 Self::Unknown
48 }
49 }
50
51 pub const fn default_ping_interval_secs(&self) -> u32 {
57 match self {
58 Self::UpdateTracker => 15,
59 Self::Scheduler => 60,
60 Self::Agent | Self::Unknown => 300,
61 }
62 }
63
64 pub const fn shutdown_timeout_secs(&self) -> Option<u32> {
70 match self {
71 Self::UpdateTracker => None,
72 Self::Scheduler => Some(30),
73 Self::Agent | Self::Unknown => Some(120),
74 }
75 }
76
77 pub const fn service_label(&self, has_ssh_remote: bool) -> &'static str {
82 match self {
83 Self::UpdateTracker => "Update Tracker",
84 Self::Scheduler => "Scheduler",
85 Self::Agent if has_ssh_remote => "SSH Agent",
86 Self::Agent => "Agent",
87 Self::Unknown => "Unknown",
88 }
89 }
90}
91
92pub fn parse_capabilities(json: &str) -> BTreeSet<Capability> {
98 serde_json::from_str::<Vec<Capability>>(json)
99 .unwrap_or_default()
100 .into_iter()
101 .collect()
102}
103
104pub fn serialize_capabilities(caps: &BTreeSet<Capability>) -> String {
110 let vec: Vec<&Capability> = caps.iter().collect();
111 serde_json::to_string(&vec).unwrap_or_else(|_| "[]".to_string())
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 fn caps(items: &[Capability]) -> BTreeSet<Capability> {
119 items.iter().cloned().collect()
120 }
121
122 #[test]
127 fn profile_update_tracker() {
128 let c = caps(&[Capability::UpdateTracking, Capability::GracefulShutdown]);
129 assert_eq!(
130 ServiceProfile::from_capabilities(&c),
131 ServiceProfile::UpdateTracker
132 );
133 }
134
135 #[test]
136 fn profile_agent() {
137 let c = caps(&[
138 Capability::SoftwareDiscovery,
139 Capability::UpdateHooks,
140 Capability::GracefulShutdown,
141 ]);
142 assert_eq!(ServiceProfile::from_capabilities(&c), ServiceProfile::Agent);
143 }
144
145 #[test]
146 fn profile_ssh_agent() {
147 let c = caps(&[
148 Capability::SoftwareDiscovery,
149 Capability::SshRemote,
150 Capability::UpdateHooks,
151 Capability::GracefulShutdown,
152 ]);
153 assert_eq!(ServiceProfile::from_capabilities(&c), ServiceProfile::Agent);
154 }
155
156 #[test]
157 fn profile_scheduler() {
158 let c = caps(&[
159 Capability::Scheduler,
160 Capability::DatabaseAccess,
161 Capability::NatsAccess,
162 Capability::GracefulShutdown,
163 ]);
164 assert_eq!(
165 ServiceProfile::from_capabilities(&c),
166 ServiceProfile::Scheduler
167 );
168 }
169
170 #[test]
171 fn scheduler_takes_precedence_over_agent() {
172 let c = caps(&[Capability::Scheduler, Capability::SoftwareDiscovery]);
173 assert_eq!(
174 ServiceProfile::from_capabilities(&c),
175 ServiceProfile::Scheduler
176 );
177 }
178
179 #[test]
180 fn update_tracker_takes_precedence_over_scheduler() {
181 let c = caps(&[Capability::UpdateTracking, Capability::Scheduler]);
182 assert_eq!(
183 ServiceProfile::from_capabilities(&c),
184 ServiceProfile::UpdateTracker
185 );
186 }
187
188 #[test]
189 fn profile_unknown_empty() {
190 assert_eq!(
191 ServiceProfile::from_capabilities(&BTreeSet::new()),
192 ServiceProfile::Unknown
193 );
194 }
195
196 #[test]
197 fn profile_unknown_only_graceful_shutdown() {
198 let c = caps(&[Capability::GracefulShutdown]);
199 assert_eq!(
200 ServiceProfile::from_capabilities(&c),
201 ServiceProfile::Unknown
202 );
203 }
204
205 #[test]
206 fn update_tracker_takes_precedence() {
207 let c = caps(&[Capability::UpdateTracking, Capability::SoftwareDiscovery]);
208 assert_eq!(
209 ServiceProfile::from_capabilities(&c),
210 ServiceProfile::UpdateTracker
211 );
212 }
213
214 #[test]
219 fn ping_interval_update_tracker() {
220 assert_eq!(
221 ServiceProfile::UpdateTracker.default_ping_interval_secs(),
222 15
223 );
224 }
225
226 #[test]
227 fn ping_interval_agent() {
228 assert_eq!(ServiceProfile::Agent.default_ping_interval_secs(), 300);
229 }
230
231 #[test]
232 fn ping_interval_scheduler() {
233 assert_eq!(ServiceProfile::Scheduler.default_ping_interval_secs(), 60);
234 }
235
236 #[test]
237 fn ping_interval_unknown() {
238 assert_eq!(ServiceProfile::Unknown.default_ping_interval_secs(), 300);
239 }
240
241 #[test]
246 fn shutdown_timeout_update_tracker() {
247 assert_eq!(ServiceProfile::UpdateTracker.shutdown_timeout_secs(), None);
248 }
249
250 #[test]
251 fn shutdown_timeout_agent() {
252 assert_eq!(ServiceProfile::Agent.shutdown_timeout_secs(), Some(120));
253 }
254
255 #[test]
256 fn shutdown_timeout_scheduler() {
257 assert_eq!(ServiceProfile::Scheduler.shutdown_timeout_secs(), Some(30));
258 }
259
260 #[test]
261 fn shutdown_timeout_unknown() {
262 assert_eq!(ServiceProfile::Unknown.shutdown_timeout_secs(), Some(120));
263 }
264
265 #[test]
270 fn label_agent() {
271 assert_eq!(ServiceProfile::Agent.service_label(false), "Agent");
272 }
273
274 #[test]
275 fn label_ssh_agent() {
276 assert_eq!(ServiceProfile::Agent.service_label(true), "SSH Agent");
277 }
278
279 #[test]
280 fn label_update_tracker() {
281 assert_eq!(
282 ServiceProfile::UpdateTracker.service_label(false),
283 "Update Tracker"
284 );
285 }
286
287 #[test]
288 fn label_scheduler() {
289 assert_eq!(ServiceProfile::Scheduler.service_label(false), "Scheduler");
290 }
291
292 #[test]
293 fn label_unknown() {
294 assert_eq!(ServiceProfile::Unknown.service_label(false), "Unknown");
295 }
296
297 #[test]
302 fn serialize_empty_set() {
303 assert_eq!(serialize_capabilities(&BTreeSet::new()), "[]");
304 }
305
306 #[test]
307 fn serialize_and_parse_round_trip() {
308 let original = caps(&[
309 Capability::GracefulShutdown,
310 Capability::SoftwareDiscovery,
311 Capability::UpdateHooks,
312 ]);
313 let json = serialize_capabilities(&original);
314 let parsed = parse_capabilities(&json);
315 assert_eq!(parsed, original);
316 }
317
318 #[test]
319 fn parse_invalid_json_returns_empty() {
320 assert!(parse_capabilities("not json").is_empty());
321 }
322
323 #[test]
324 fn parse_empty_array() {
325 assert!(parse_capabilities("[]").is_empty());
326 }
327
328 #[test]
329 fn parse_preserves_all_known_capabilities() {
330 let json = r#"["graceful_shutdown","software_discovery","ssh_remote","update_hooks","update_tracking"]"#;
331 let parsed = parse_capabilities(json);
332 assert_eq!(parsed.len(), 5);
333 assert!(parsed.contains(&Capability::GracefulShutdown));
334 assert!(parsed.contains(&Capability::UpdateTracking));
335 assert!(parsed.contains(&Capability::SoftwareDiscovery));
336 assert!(parsed.contains(&Capability::SshRemote));
337 assert!(parsed.contains(&Capability::UpdateHooks));
338 }
339
340 #[test]
341 fn parse_unknown_capabilities_become_other() {
342 let json = r#"["software_discovery","future_cap"]"#;
343 let parsed = parse_capabilities(json);
344 assert_eq!(parsed.len(), 2);
345 assert!(parsed.contains(&Capability::SoftwareDiscovery));
346 assert!(parsed.contains(&Capability::Other("future_cap".to_string())));
347 }
348
349 #[test]
350 fn serialize_produces_sorted_output() {
351 let c = caps(&[
352 Capability::UpdateHooks,
353 Capability::GracefulShutdown,
354 Capability::SoftwareDiscovery,
355 ]);
356 let json = serialize_capabilities(&c);
357 let parsed: Vec<String> = serde_json::from_str(&json).unwrap_or_default();
359 let mut sorted = parsed.clone();
360 sorted.sort();
361 assert_eq!(parsed, sorted);
362 }
363}