1use async_trait::async_trait;
2use std::path::{Path, PathBuf};
3
4use crate::config::ShimConfig;
5use crate::error::Result;
6
7pub type PreExecuteHook = Box<dyn Fn(&[String]) -> Result<()> + Send + Sync>;
9
10pub type PostExecuteHook = Box<dyn Fn(i32) -> Result<()> + Send + Sync>;
12
13pub trait ShimConfigLoader: Send + Sync {
15 fn load_config(&self, path: &Path) -> Result<ShimConfig>;
17
18 fn save_config(&self, config: &ShimConfig, path: &Path) -> Result<()>;
20
21 fn file_extension(&self) -> &str;
23
24 fn validate_config(&self, config: &ShimConfig) -> Result<()> {
26 config.validate()
27 }
28}
29
30#[async_trait]
32pub trait ShimRunnerTrait {
33 async fn execute(&self, additional_args: &[String]) -> Result<i32>;
35
36 fn config(&self) -> &ShimConfig;
38
39 fn validate(&self) -> Result<()>;
41
42 async fn pre_execute(&self, _args: &[String]) -> Result<()> {
44 Ok(())
45 }
46
47 async fn post_execute(&self, _exit_code: i32) -> Result<()> {
49 Ok(())
50 }
51}
52
53#[async_trait]
55pub trait UpdateProvider: Send + Sync {
56 async fn check_update_available(&self, current_version: &str) -> Result<Option<String>>;
58
59 async fn install_update(&self, version: &str, target_path: &Path) -> Result<()>;
61
62 fn get_download_url(&self, version: &str) -> Result<String>;
64
65 async fn verify_download(&self, _file_path: &Path, _version: &str) -> Result<bool> {
67 Ok(true)
69 }
70}
71
72#[async_trait]
74pub trait VersionChecker: Send + Sync {
75 async fn get_latest_version(&self) -> Result<String>;
77
78 fn is_newer_version(&self, version1: &str, version2: &str) -> Result<bool>;
80
81 fn parse_version(&self, version_str: &str) -> Result<String>;
83}
84
85pub struct ShimRunnerBuilder {
87 config_loader: Option<Box<dyn ShimConfigLoader>>,
88 update_provider: Option<Box<dyn UpdateProvider>>,
89 version_checker: Option<Box<dyn VersionChecker>>,
90 config_file_pattern: Option<String>,
91 pre_execute_hooks: Vec<PreExecuteHook>,
92 post_execute_hooks: Vec<PostExecuteHook>,
93}
94
95impl ShimRunnerBuilder {
96 pub fn new() -> Self {
98 Self {
99 config_loader: None,
100 update_provider: None,
101 version_checker: None,
102 config_file_pattern: None,
103 pre_execute_hooks: Vec::new(),
104 post_execute_hooks: Vec::new(),
105 }
106 }
107
108 pub fn with_config_loader(mut self, loader: Box<dyn ShimConfigLoader>) -> Self {
110 self.config_loader = Some(loader);
111 self
112 }
113
114 pub fn with_update_provider(mut self, provider: Box<dyn UpdateProvider>) -> Self {
116 self.update_provider = Some(provider);
117 self
118 }
119
120 pub fn with_version_checker(mut self, checker: Box<dyn VersionChecker>) -> Self {
122 self.version_checker = Some(checker);
123 self
124 }
125
126 pub fn with_config_file_pattern<S: Into<String>>(mut self, pattern: S) -> Self {
128 self.config_file_pattern = Some(pattern.into());
129 self
130 }
131
132 pub fn with_pre_execute_hook<F>(mut self, hook: F) -> Self
134 where
135 F: Fn(&[String]) -> Result<()> + Send + Sync + 'static,
136 {
137 self.pre_execute_hooks.push(Box::new(hook));
138 self
139 }
140
141 pub fn with_post_execute_hook<F>(mut self, hook: F) -> Self
143 where
144 F: Fn(i32) -> Result<()> + Send + Sync + 'static,
145 {
146 self.post_execute_hooks.push(Box::new(hook));
147 self
148 }
149
150 pub fn build(self, shim_name: &str, shim_dir: &Path) -> Result<CustomizableShimRunner> {
152 let config_file_pattern = self
153 .config_file_pattern
154 .unwrap_or_else(|| "{name}.shim.toml".to_string());
155
156 let config_file = shim_dir.join(config_file_pattern.replace("{name}", shim_name));
157
158 let config_loader = self
159 .config_loader
160 .unwrap_or_else(|| Box::new(DefaultConfigLoader));
161
162 let config = config_loader.load_config(&config_file)?;
163
164 Ok(CustomizableShimRunner {
165 config,
166 config_file_path: config_file,
167 config_loader,
168 update_provider: self.update_provider,
169 version_checker: self.version_checker,
170 pre_execute_hooks: self.pre_execute_hooks,
171 post_execute_hooks: self.post_execute_hooks,
172 })
173 }
174}
175
176impl Default for ShimRunnerBuilder {
177 fn default() -> Self {
178 Self::new()
179 }
180}
181
182pub struct CustomizableShimRunner {
184 config: ShimConfig,
185 #[allow(dead_code)]
186 config_file_path: PathBuf,
187 config_loader: Box<dyn ShimConfigLoader>,
188 update_provider: Option<Box<dyn UpdateProvider>>,
189 version_checker: Option<Box<dyn VersionChecker>>,
190 pre_execute_hooks: Vec<PreExecuteHook>,
191 post_execute_hooks: Vec<PostExecuteHook>,
192}
193
194#[async_trait]
195impl ShimRunnerTrait for CustomizableShimRunner {
196 async fn execute(&self, additional_args: &[String]) -> Result<i32> {
197 for hook in &self.pre_execute_hooks {
199 hook(additional_args)?;
200 }
201
202 if let (Some(ref update_provider), Some(ref version_checker)) =
204 (&self.update_provider, &self.version_checker)
205 {
206 self.check_and_update(update_provider.as_ref(), version_checker.as_ref())
207 .await?;
208 }
209
210 let executable_path = self.config.get_executable_path()?;
212 let exit_code = self.run_executable(&executable_path, additional_args)?;
213
214 for hook in &self.post_execute_hooks {
216 hook(exit_code)?;
217 }
218
219 Ok(exit_code)
220 }
221
222 fn config(&self) -> &ShimConfig {
223 &self.config
224 }
225
226 fn validate(&self) -> Result<()> {
227 self.config_loader.validate_config(&self.config)
228 }
229}
230
231impl CustomizableShimRunner {
232 async fn check_and_update(
234 &self,
235 update_provider: &dyn UpdateProvider,
236 version_checker: &dyn VersionChecker,
237 ) -> Result<()> {
238 let current_version = "1.0.0"; if let Some(latest_version) = update_provider
241 .check_update_available(current_version)
242 .await?
243 {
244 if version_checker.is_newer_version(&latest_version, current_version)? {
245 let executable_path = self.config.get_executable_path()?;
246 update_provider
247 .install_update(&latest_version, &executable_path)
248 .await?;
249 }
250 }
251
252 Ok(())
253 }
254
255 fn run_executable(&self, executable_path: &Path, args: &[String]) -> Result<i32> {
257 use std::process::Command;
259
260 let status = Command::new(executable_path)
261 .args(args)
262 .status()
263 .map_err(|e| crate::error::ShimError::ProcessExecution(e.to_string()))?;
264
265 Ok(status.code().unwrap_or(-1))
266 }
267}
268
269pub struct DefaultConfigLoader;
271
272impl ShimConfigLoader for DefaultConfigLoader {
273 fn load_config(&self, path: &Path) -> Result<ShimConfig> {
274 ShimConfig::from_file(path)
275 }
276
277 fn save_config(&self, config: &ShimConfig, path: &Path) -> Result<()> {
278 config.to_file(path)
279 }
280
281 fn file_extension(&self) -> &str {
282 "shim.toml"
283 }
284}