1use anyhow::{bail, Result};
2use std::collections::HashMap;
3
4pub fn expand(template: &str, vars: &HashMap<String, String>) -> Result<String> {
5 let mut result = template.to_string();
6 let mut i = 0;
8 let bytes = result.as_bytes().to_vec();
9 let mut out = String::with_capacity(result.len());
10 let s = std::str::from_utf8(&bytes).unwrap();
11 let chars: Vec<char> = s.chars().collect();
12 while i < chars.len() {
13 if chars[i] == '$' && i + 1 < chars.len() && chars[i + 1] == '{' {
14 let start = i + 2;
15 if let Some(end) = chars[start..].iter().position(|&c| c == '}') {
16 let name: String = chars[start..start + end].iter().collect();
17 match vars.get(&name) {
18 Some(val) => out.push_str(val),
19 None => bail!("undefined variable: ${{{}}}", name),
20 }
21 i = start + end + 1;
22 continue;
23 }
24 }
25 out.push(chars[i]);
26 i += 1;
27 }
28 result = out;
29 Ok(result)
30}
31
32pub fn resolve(
33 skill_name: &str,
34 skill_path: &str,
35 custom: &HashMap<String, String>,
36) -> Result<HashMap<String, String>> {
37 let mut vars = HashMap::new();
38 vars.insert("SKILL_NAME".into(), skill_name.into());
39 vars.insert("SKILL_PATH".into(), skill_path.into());
40 vars.insert("HOME".into(), std::env::var("HOME").unwrap_or_default());
41 vars.insert(
42 "PLATFORM".into(),
43 if cfg!(target_os = "macos") {
44 "macos"
45 } else if cfg!(target_os = "windows") {
46 "windows"
47 } else {
48 "linux"
49 }
50 .into(),
51 );
52 for (k, v) in custom {
55 let expanded = expand(v, &vars)?;
56 vars.insert(k.clone(), expanded);
57 }
58 Ok(vars)
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 fn base_vars() -> HashMap<String, String> {
66 let mut m = HashMap::new();
67 m.insert("SKILL_PATH".into(), "/skills/my-skill".into());
68 m.insert("HOME".into(), "/home/user".into());
69 m
70 }
71
72 #[test]
73 fn expand_builtin() {
74 let vars = base_vars();
75 assert_eq!(
76 expand("${SKILL_PATH}/.venv", &vars).unwrap(),
77 "/skills/my-skill/.venv"
78 );
79 }
80
81 #[test]
82 fn expand_multiple() {
83 let vars = base_vars();
84 assert_eq!(
85 expand("${HOME}/${SKILL_PATH}", &vars).unwrap(),
86 "/home/user//skills/my-skill"
87 );
88 }
89
90 #[test]
91 fn expand_undefined_errors() {
92 let vars = base_vars();
93 assert!(expand("${UNDEFINED}", &vars).is_err());
94 }
95
96 #[test]
97 fn resolve_builtins_present() {
98 let vars = resolve("my-skill", "/skills/my-skill", &HashMap::new()).unwrap();
99 assert_eq!(vars["SKILL_NAME"], "my-skill");
100 assert_eq!(vars["SKILL_PATH"], "/skills/my-skill");
101 assert!(vars.contains_key("HOME"));
102 assert!(vars.contains_key("PLATFORM"));
103 }
104
105 #[test]
106 fn resolve_custom_expands_against_builtins() {
107 let mut custom = HashMap::new();
108 custom.insert("VENV".into(), "${SKILL_PATH}/.venv".into());
109 let vars = resolve("s", "/p", &custom).unwrap();
110 assert_eq!(vars["VENV"], "/p/.venv");
111 }
112
113 #[test]
114 fn resolve_custom_undefined_ref_errors() {
115 let mut custom = HashMap::new();
116 custom.insert("X".into(), "${UNDEFINED}".into());
117 assert!(resolve("s", "/p", &custom).is_err());
118 }
119}