1#[derive(Debug, Clone)]
4pub enum SkillAvailability {
6 Ready,
7 Unavailable(String),
8}
9
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
11pub struct SkillRequirements {
13 pub binaries: Vec<String>,
14 pub env_vars: Vec<String>,
15 pub min_version: Option<String>,
16}
17
18impl SkillRequirements {
19 pub fn new() -> Self {
21 Self {
22 binaries: Vec::new(),
23 env_vars: Vec::new(),
24 min_version: None,
25 }
26 }
27
28 pub fn check(&self) -> SkillAvailability {
30 for bin in &self.binaries {
31 if std::process::Command::new("which")
32 .arg(bin)
33 .output()
34 .map(|o| !o.status.success())
35 .unwrap_or(true)
36 {
37 return SkillAvailability::Unavailable(format!("missing binary: {}", bin));
38 }
39 }
40 for var in &self.env_vars {
41 if std::env::var(var).is_err() {
42 return SkillAvailability::Unavailable(format!("missing env var: {}", var));
43 }
44 }
45 SkillAvailability::Ready
46 }
47}
48
49impl Default for SkillRequirements {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn skill_requirements_empty_is_ready() {
61 let reqs = SkillRequirements::new();
62 assert!(matches!(reqs.check(), SkillAvailability::Ready));
63 }
64
65 #[test]
66 fn skill_requirements_missing_binary() {
67 let reqs = SkillRequirements {
68 binaries: vec!["nonexistent_binary_xyz_12345".into()],
69 env_vars: vec![],
70 min_version: None,
71 };
72 match reqs.check() {
73 SkillAvailability::Unavailable(msg) => assert!(msg.contains("missing binary")),
74 _ => panic!("expected Unavailable"),
75 }
76 }
77
78 #[test]
79 fn skill_requirements_present_binary() {
80 let reqs = SkillRequirements {
81 binaries: vec!["ls".into()],
82 env_vars: vec![],
83 min_version: None,
84 };
85 assert!(matches!(reqs.check(), SkillAvailability::Ready));
86 }
87
88 #[test]
89 fn skill_requirements_missing_env_var() {
90 let reqs = SkillRequirements {
91 binaries: vec![],
92 env_vars: vec!["TOTALLY_FAKE_ENV_VAR_12345678".into()],
93 min_version: None,
94 };
95 match reqs.check() {
96 SkillAvailability::Unavailable(msg) => assert!(msg.contains("missing env var")),
97 _ => panic!("expected Unavailable"),
98 }
99 }
100
101 #[test]
102 fn skill_requirements_present_env_var() {
103 let reqs = SkillRequirements {
105 binaries: vec![],
106 env_vars: vec!["HOME".into()],
107 min_version: None,
108 };
109 assert!(matches!(reqs.check(), SkillAvailability::Ready));
110 }
111
112 #[test]
113 fn skill_requirements_combined() {
114 let reqs = SkillRequirements {
115 binaries: vec!["ls".into()],
116 env_vars: vec!["HOME".into()],
117 min_version: Some("1.0.0".into()),
118 };
119 assert!(matches!(reqs.check(), SkillAvailability::Ready));
120 }
121
122 #[test]
123 fn skill_requirements_default() {
124 let reqs = SkillRequirements::default();
125 assert!(matches!(reqs.check(), SkillAvailability::Ready));
126 }
127
128 #[test]
129 fn skill_availability_debug() {
130 let avail = SkillAvailability::Ready;
131 assert!(format!("{:?}", avail).contains("Ready"));
132
133 let unavail = SkillAvailability::Unavailable("test reason".into());
134 assert!(format!("{:?}", unavail).contains("test reason"));
135 }
136}