shimexe_core/
template.rs

1use serde::{Deserialize, Serialize};
2use std::env;
3use std::path::Path;
4
5use crate::error::{Result, ShimError};
6
7/// Template engine for processing dynamic configuration
8pub struct TemplateEngine {
9    user_args: Vec<String>,
10}
11
12/// Template-based argument configuration
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ArgsConfig {
15    /// Template-based argument list
16    #[serde(default)]
17    pub template: Option<Vec<String>>,
18
19    /// Inline template string
20    #[serde(default)]
21    pub inline: Option<String>,
22
23    /// Argument processing mode
24    #[serde(default)]
25    pub mode: ArgsMode,
26
27    /// Default arguments (legacy support)
28    #[serde(default)]
29    pub default: Vec<String>,
30
31    /// Arguments always prepended
32    #[serde(default)]
33    pub prefix: Vec<String>,
34
35    /// Arguments always appended
36    #[serde(default)]
37    pub suffix: Vec<String>,
38}
39
40/// Argument processing modes
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
42#[serde(rename_all = "lowercase")]
43pub enum ArgsMode {
44    #[default]
45    Template, // Use template-based processing
46    Merge,   // Combine default + user args
47    Replace, // User args replace default
48    Prepend, // User args + default args
49}
50
51impl Default for ArgsConfig {
52    fn default() -> Self {
53        Self {
54            template: None,
55            inline: None,
56            mode: ArgsMode::Template,
57            default: Vec::new(),
58            prefix: Vec::new(),
59            suffix: Vec::new(),
60        }
61    }
62}
63
64impl TemplateEngine {
65    /// Create a new template engine with user arguments
66    pub fn new(user_args: Vec<String>) -> Self {
67        Self { user_args }
68    }
69
70    /// Process arguments based on configuration
71    pub fn process_args(&mut self, args_config: &ArgsConfig) -> Result<Vec<String>> {
72        match args_config.mode {
73            ArgsMode::Template => {
74                if let Some(ref template) = args_config.template {
75                    self.render_template_args(template)
76                } else if let Some(ref inline) = args_config.inline {
77                    self.render_inline_template(inline)
78                } else {
79                    // Fallback to user args or empty
80                    Ok(self.user_args.clone())
81                }
82            }
83            ArgsMode::Merge => {
84                let mut result = args_config.prefix.clone();
85                result.extend(args_config.default.clone());
86                result.extend(self.user_args.clone());
87                result.extend(args_config.suffix.clone());
88                Ok(result)
89            }
90            ArgsMode::Replace => {
91                let mut result = args_config.prefix.clone();
92                if self.user_args.is_empty() {
93                    result.extend(args_config.default.clone());
94                } else {
95                    result.extend(self.user_args.clone());
96                }
97                result.extend(args_config.suffix.clone());
98                Ok(result)
99            }
100            ArgsMode::Prepend => {
101                let mut result = args_config.prefix.clone();
102                result.extend(self.user_args.clone());
103                result.extend(args_config.default.clone());
104                result.extend(args_config.suffix.clone());
105                Ok(result)
106            }
107        }
108    }
109
110    /// Render template arguments
111    fn render_template_args(&mut self, template: &[String]) -> Result<Vec<String>> {
112        let mut result = Vec::new();
113
114        for template_arg in template {
115            let rendered = self.render_template(template_arg)?;
116            if !rendered.is_empty() {
117                // Split on whitespace for inline templates
118                if rendered.contains(' ') {
119                    result.extend(rendered.split_whitespace().map(String::from));
120                } else {
121                    result.push(rendered);
122                }
123            }
124        }
125
126        Ok(result)
127    }
128
129    /// Render inline template
130    fn render_inline_template(&mut self, template: &str) -> Result<Vec<String>> {
131        let rendered = self.render_template(template)?;
132        Ok(rendered.split_whitespace().map(String::from).collect())
133    }
134
135    /// Render a single template string
136    pub fn render_template(&mut self, template: &str) -> Result<String> {
137        let mut result = template.to_string();
138
139        // Process template expressions {{...}}
140        while let Some(start) = result.find("{{") {
141            if let Some(end) = result[start..].find("}}") {
142                let expr_end = start + end + 2;
143                let expression = &result[start + 2..start + end];
144
145                let value = self.evaluate_expression(expression)?;
146                result.replace_range(start..expr_end, &value);
147            } else {
148                break;
149            }
150        }
151
152        Ok(result)
153    }
154
155    /// Evaluate a template expression
156    fn evaluate_expression(&mut self, expr: &str) -> Result<String> {
157        let expr = expr.trim();
158
159        // Handle simple cases first
160        if expr == "args" {
161            return Ok(self.user_args.join(" "));
162        }
163
164        // Handle args with default: args('default')
165        if expr.starts_with("args(") && expr.ends_with(")") {
166            let default = &expr[5..expr.len() - 1];
167            let default = default.trim_matches('\'').trim_matches('"');
168
169            if self.user_args.is_empty() {
170                return Ok(default.to_string());
171            } else {
172                return Ok(self.user_args.join(" "));
173            }
174        }
175
176        // Handle env() function
177        if expr.starts_with("env(") && expr.ends_with(")") {
178            return self.evaluate_env_function(expr);
179        }
180
181        // Handle if conditions
182        if expr.starts_with("if ") {
183            return self.evaluate_if_condition(expr);
184        }
185
186        // Handle function calls
187        if expr.contains("()") {
188            return self.evaluate_function_call(expr);
189        }
190
191        // Default: return as-is
192        Ok(expr.to_string())
193    }
194
195    /// Evaluate env() function calls
196    fn evaluate_env_function(&mut self, expr: &str) -> Result<String> {
197        let inner = &expr[4..expr.len() - 1]; // Remove "env(" and ")"
198
199        if inner.contains(',') {
200            // env('VAR', 'default')
201            let parts: Vec<&str> = inner.split(',').collect();
202            if parts.len() == 2 {
203                let var_name = parts[0].trim().trim_matches('\'').trim_matches('"');
204                let default = parts[1].trim().trim_matches('\'').trim_matches('"');
205
206                Ok(env::var(var_name).unwrap_or_else(|_| default.to_string()))
207            } else {
208                Err(ShimError::TemplateError(format!(
209                    "Invalid env() syntax: {}",
210                    expr
211                )))
212            }
213        } else {
214            // env('VAR')
215            let var_name = inner.trim().trim_matches('\'').trim_matches('"');
216            Ok(env::var(var_name).unwrap_or_default())
217        }
218    }
219
220    /// Evaluate if conditions
221    fn evaluate_if_condition(&mut self, expr: &str) -> Result<String> {
222        // Simple if condition parsing
223        // Format: if condition}}content{{endif
224        // For now, just handle basic env comparisons
225
226        if expr.contains("env(") && expr.contains("==") {
227            // Extract condition
228            let condition_part = expr.strip_prefix("if ").unwrap_or(expr);
229
230            // Very basic parsing for env('VAR') == 'value'
231            if let Some(eq_pos) = condition_part.find("==") {
232                let left = condition_part[..eq_pos].trim();
233                let right = condition_part[eq_pos + 2..]
234                    .trim()
235                    .trim_matches('\'')
236                    .trim_matches('"');
237
238                if left.starts_with("env(") && left.ends_with(")") {
239                    let env_value = self.evaluate_env_function(left)?;
240                    if env_value == right {
241                        return Ok("true".to_string());
242                    }
243                }
244            }
245        }
246
247        Ok("false".to_string())
248    }
249
250    /// Evaluate function calls
251    fn evaluate_function_call(&mut self, expr: &str) -> Result<String> {
252        match expr {
253            "platform()" => Ok(self.get_platform()),
254            "arch()" => Ok(self.get_arch()),
255            "exe_ext()" => Ok(self.get_exe_ext()),
256            "home_dir()" => Ok(self.get_home_dir()),
257            _ => {
258                if expr.starts_with("file_exists(") && expr.ends_with(")") {
259                    let path = &expr[12..expr.len() - 1];
260                    let path = path.trim_matches('\'').trim_matches('"');
261                    Ok(Path::new(path).exists().to_string())
262                } else {
263                    Ok(expr.to_string())
264                }
265            }
266        }
267    }
268
269    /// Get current platform
270    fn get_platform(&self) -> String {
271        if cfg!(target_os = "windows") {
272            "windows".to_string()
273        } else if cfg!(target_os = "macos") {
274            "macos".to_string()
275        } else if cfg!(target_os = "linux") {
276            "linux".to_string()
277        } else {
278            "unknown".to_string()
279        }
280    }
281
282    /// Get current architecture
283    fn get_arch(&self) -> String {
284        if cfg!(target_arch = "x86_64") {
285            "x86_64".to_string()
286        } else if cfg!(target_arch = "aarch64") {
287            "aarch64".to_string()
288        } else {
289            "unknown".to_string()
290        }
291    }
292
293    /// Get executable extension
294    fn get_exe_ext(&self) -> String {
295        if cfg!(target_os = "windows") {
296            ".exe".to_string()
297        } else {
298            "".to_string()
299        }
300    }
301
302    /// Get home directory
303    fn get_home_dir(&self) -> String {
304        env::var("HOME")
305            .or_else(|_| env::var("USERPROFILE"))
306            .unwrap_or_else(|_| ".".to_string())
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn test_args_mode_merge() {
316        let mut engine = TemplateEngine::new(vec!["user1".to_string(), "user2".to_string()]);
317
318        let config = ArgsConfig {
319            mode: ArgsMode::Merge,
320            default: vec!["default1".to_string(), "default2".to_string()],
321            prefix: vec!["prefix".to_string()],
322            suffix: vec!["suffix".to_string()],
323            ..Default::default()
324        };
325
326        let result = engine.process_args(&config).unwrap();
327        assert_eq!(
328            result,
329            vec!["prefix", "default1", "default2", "user1", "user2", "suffix"]
330        );
331    }
332
333    #[test]
334    fn test_args_mode_replace() {
335        let mut engine = TemplateEngine::new(vec!["user1".to_string()]);
336
337        let config = ArgsConfig {
338            mode: ArgsMode::Replace,
339            default: vec!["default".to_string()],
340            prefix: vec!["prefix".to_string()],
341            ..Default::default()
342        };
343
344        let result = engine.process_args(&config).unwrap();
345        assert_eq!(result, vec!["prefix", "user1"]);
346
347        // Test with no user args
348        let mut engine_empty = TemplateEngine::new(vec![]);
349        let result_empty = engine_empty.process_args(&config).unwrap();
350        assert_eq!(result_empty, vec!["prefix", "default"]);
351    }
352
353    #[test]
354    fn test_template_args_basic() {
355        let mut engine = TemplateEngine::new(vec!["--help".to_string()]);
356
357        let config = ArgsConfig {
358            mode: ArgsMode::Template,
359            template: Some(vec![
360                "{{args('--version')}}".to_string(),
361                "--verbose".to_string(),
362            ]),
363            ..Default::default()
364        };
365
366        let result = engine.process_args(&config).unwrap();
367        assert_eq!(result, vec!["--help", "--verbose"]);
368    }
369
370    #[test]
371    fn test_template_args_with_default() {
372        let mut engine = TemplateEngine::new(vec![]);
373
374        let config = ArgsConfig {
375            mode: ArgsMode::Template,
376            template: Some(vec!["{{args('--version')}}".to_string()]),
377            ..Default::default()
378        };
379
380        let result = engine.process_args(&config).unwrap();
381        assert_eq!(result, vec!["--version"]);
382    }
383
384    #[test]
385    fn test_env_function() {
386        env::set_var("TEST_TEMPLATE_VAR", "test_value");
387
388        let mut engine = TemplateEngine::new(vec![]);
389
390        let result = engine
391            .evaluate_env_function("env('TEST_TEMPLATE_VAR')")
392            .unwrap();
393        assert_eq!(result, "test_value");
394
395        let result_with_default = engine
396            .evaluate_env_function("env('NONEXISTENT', 'default')")
397            .unwrap();
398        assert_eq!(result_with_default, "default");
399
400        env::remove_var("TEST_TEMPLATE_VAR");
401    }
402
403    #[test]
404    fn test_platform_functions() {
405        let mut engine = TemplateEngine::new(vec![]);
406
407        let platform = engine.evaluate_function_call("platform()").unwrap();
408        assert!(["windows", "linux", "macos", "unknown"].contains(&platform.as_str()));
409
410        let arch = engine.evaluate_function_call("arch()").unwrap();
411        assert!(["x86_64", "aarch64", "unknown"].contains(&arch.as_str()));
412
413        let exe_ext = engine.evaluate_function_call("exe_ext()").unwrap();
414        if cfg!(target_os = "windows") {
415            assert_eq!(exe_ext, ".exe");
416        } else {
417            assert_eq!(exe_ext, "");
418        }
419    }
420
421    #[test]
422    fn test_file_exists_function() {
423        let mut engine = TemplateEngine::new(vec![]);
424
425        // Test with a file that should exist (Cargo.toml in project root)
426        let result = engine
427            .evaluate_function_call("file_exists('Cargo.toml')")
428            .unwrap();
429        // Note: This might be "false" depending on test execution context
430        assert!(result == "true" || result == "false");
431
432        // Test with a file that definitely doesn't exist
433        let result = engine
434            .evaluate_function_call("file_exists('definitely_not_exists.xyz')")
435            .unwrap();
436        assert_eq!(result, "false");
437    }
438
439    #[test]
440    fn test_render_template_complete() {
441        env::set_var("TEST_ENV", "production");
442
443        let mut engine = TemplateEngine::new(vec!["--input".to_string(), "file.txt".to_string()]);
444
445        let template = "--env {{env('TEST_ENV', 'development')}} {{args('--help')}}";
446        let result = engine.render_template(template).unwrap();
447        assert_eq!(result, "--env production --input file.txt");
448
449        env::remove_var("TEST_ENV");
450    }
451}