1use serde::{Deserialize, Serialize};
2use std::env;
3use std::path::Path;
4
5use crate::error::{Result, ShimError};
6
7pub struct TemplateEngine {
9 user_args: Vec<String>,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ArgsConfig {
15 #[serde(default)]
17 pub template: Option<Vec<String>>,
18
19 #[serde(default)]
21 pub inline: Option<String>,
22
23 #[serde(default)]
25 pub mode: ArgsMode,
26
27 #[serde(default)]
29 pub default: Vec<String>,
30
31 #[serde(default)]
33 pub prefix: Vec<String>,
34
35 #[serde(default)]
37 pub suffix: Vec<String>,
38}
39
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
42#[serde(rename_all = "lowercase")]
43pub enum ArgsMode {
44 #[default]
45 Template, Merge, Replace, Prepend, }
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 pub fn new(user_args: Vec<String>) -> Self {
67 Self { user_args }
68 }
69
70 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 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 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 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 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 pub fn render_template(&mut self, template: &str) -> Result<String> {
137 let mut result = template.to_string();
138
139 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 fn evaluate_expression(&mut self, expr: &str) -> Result<String> {
157 let expr = expr.trim();
158
159 if expr == "args" {
161 return Ok(self.user_args.join(" "));
162 }
163
164 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 if expr.starts_with("env(") && expr.ends_with(")") {
178 return self.evaluate_env_function(expr);
179 }
180
181 if expr.starts_with("if ") {
183 return self.evaluate_if_condition(expr);
184 }
185
186 if expr.contains("()") {
188 return self.evaluate_function_call(expr);
189 }
190
191 Ok(expr.to_string())
193 }
194
195 fn evaluate_env_function(&mut self, expr: &str) -> Result<String> {
197 let inner = &expr[4..expr.len() - 1]; if inner.contains(',') {
200 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 let var_name = inner.trim().trim_matches('\'').trim_matches('"');
216 Ok(env::var(var_name).unwrap_or_default())
217 }
218 }
219
220 fn evaluate_if_condition(&mut self, expr: &str) -> Result<String> {
222 if expr.contains("env(") && expr.contains("==") {
227 let condition_part = expr.strip_prefix("if ").unwrap_or(expr);
229
230 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 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 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 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 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 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 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 let result = engine
427 .evaluate_function_call("file_exists('Cargo.toml')")
428 .unwrap();
429 assert!(result == "true" || result == "false");
431
432 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}