1use crate::badge::SessionVariables;
10use crate::config::snippets::BuiltInVariable;
11use regex::Regex;
12use std::collections::HashMap;
13
14#[derive(Debug, Clone)]
16pub enum SubstitutionError {
17 InvalidVariable(String),
19 UndefinedVariable(String),
21}
22
23impl std::fmt::Display for SubstitutionError {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 Self::InvalidVariable(name) => write!(f, "Invalid variable name: {}", name),
27 Self::UndefinedVariable(name) => write!(f, "Undefined variable: {}", name),
28 }
29 }
30}
31
32impl std::error::Error for SubstitutionError {}
33
34pub type SubstitutionResult<T> = Result<T, SubstitutionError>;
36
37pub struct VariableSubstitutor {
43 pattern: Regex,
45}
46
47impl VariableSubstitutor {
48 pub fn new() -> Self {
50 let pattern = Regex::new(r"\\\(([a-zA-Z_][a-zA-Z0-9_.]*)\)").unwrap();
53
54 Self { pattern }
55 }
56
57 pub fn substitute(
66 &self,
67 text: &str,
68 custom_vars: &HashMap<String, String>,
69 ) -> SubstitutionResult<String> {
70 self.substitute_with_session(text, custom_vars, None)
71 }
72
73 pub fn substitute_with_session(
83 &self,
84 text: &str,
85 custom_vars: &HashMap<String, String>,
86 session_vars: Option<&SessionVariables>,
87 ) -> SubstitutionResult<String> {
88 let mut result = text.to_string();
89
90 for cap in self.pattern.captures_iter(text) {
92 let full_match = cap.get(0).unwrap().as_str();
93 let var_name = cap.get(1).unwrap().as_str();
94
95 let value = self.resolve_variable_with_session(var_name, custom_vars, session_vars)?;
97
98 result = result.replace(full_match, &value);
100 }
101
102 Ok(result)
103 }
104
105 #[allow(dead_code)]
107 fn resolve_variable(
108 &self,
109 name: &str,
110 custom_vars: &HashMap<String, String>,
111 ) -> SubstitutionResult<String> {
112 self.resolve_variable_with_session(name, custom_vars, None)
113 }
114
115 fn resolve_variable_with_session(
117 &self,
118 name: &str,
119 custom_vars: &HashMap<String, String>,
120 session_vars: Option<&SessionVariables>,
121 ) -> SubstitutionResult<String> {
122 if let Some(value) = custom_vars.get(name) {
124 return Ok(value.clone());
125 }
126
127 if let Some(value) = session_vars.and_then(|session| session.get(name)) {
129 return Ok(value);
130 }
131
132 if let Some(builtin) = BuiltInVariable::parse(name) {
134 return Ok(builtin.resolve());
135 }
136
137 Err(SubstitutionError::UndefinedVariable(name.to_string()))
139 }
140
141 pub fn has_variables(&self, text: &str) -> bool {
143 self.pattern.is_match(text)
144 }
145
146 pub fn extract_variables(&self, text: &str) -> Vec<String> {
148 self.pattern
149 .captures_iter(text)
150 .map(|cap| cap.get(1).unwrap().as_str().to_string())
151 .collect()
152 }
153}
154
155impl Default for VariableSubstitutor {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_substitute_builtin_variables() {
167 let substitutor = VariableSubstitutor::new();
168 let custom_vars = HashMap::new();
169
170 let result = substitutor
172 .substitute("Today is \\(date)", &custom_vars)
173 .unwrap();
174 assert!(result.starts_with("Today is "));
175 assert!(!result.contains("\\(date)"));
176
177 let result = substitutor
179 .substitute("User: \\(user)", &custom_vars)
180 .unwrap();
181 assert!(result.starts_with("User: "));
182 assert!(!result.contains("\\(user)"));
183 }
184
185 #[test]
186 fn test_substitute_custom_variables() {
187 let substitutor = VariableSubstitutor::new();
188 let mut custom_vars = HashMap::new();
189 custom_vars.insert("name".to_string(), "Alice".to_string());
190 custom_vars.insert("project".to_string(), "par-term".to_string());
191
192 let result = substitutor
193 .substitute("Hello \\(name), welcome to \\(project)!", &custom_vars)
194 .unwrap();
195
196 assert_eq!(result, "Hello Alice, welcome to par-term!");
197 }
198
199 #[test]
200 fn test_substitute_mixed_variables() {
201 let substitutor = VariableSubstitutor::new();
202 let mut custom_vars = HashMap::new();
203 custom_vars.insert("greeting".to_string(), "Hello".to_string());
204
205 let result = substitutor
206 .substitute("\\(greeting) \\(user), today is \\(date)", &custom_vars)
207 .unwrap();
208
209 assert!(result.starts_with("Hello "));
210 assert!(!result.contains("\\("));
211 }
212
213 #[test]
214 fn test_undefined_variable() {
215 let substitutor = VariableSubstitutor::new();
216 let custom_vars = HashMap::new();
217
218 let result = substitutor.substitute("Value: \\(undefined)", &custom_vars);
219
220 assert!(result.is_err());
221 match result.unwrap_err() {
222 SubstitutionError::UndefinedVariable(name) => assert_eq!(name, "undefined"),
223 _ => panic!("Expected UndefinedVariable error"),
224 }
225 }
226
227 #[test]
228 fn test_has_variables() {
229 let substitutor = VariableSubstitutor::new();
230
231 assert!(substitutor.has_variables("Hello \\(user)"));
232 assert!(!substitutor.has_variables("Hello world"));
233 }
234
235 #[test]
236 fn test_extract_variables() {
237 let substitutor = VariableSubstitutor::new();
238
239 let vars = substitutor.extract_variables("Hello \\(user), today is \\(date)");
240 assert_eq!(vars, vec!["user", "date"]);
241 }
242
243 #[test]
244 fn test_no_variables() {
245 let substitutor = VariableSubstitutor::new();
246 let custom_vars = HashMap::new();
247
248 let result = substitutor
249 .substitute("Just plain text with no variables", &custom_vars)
250 .unwrap();
251
252 assert_eq!(result, "Just plain text with no variables");
253 }
254
255 #[test]
256 fn test_empty_custom_vars() {
257 let substitutor = VariableSubstitutor::new();
258 let custom_vars = HashMap::new();
259
260 let result = substitutor
261 .substitute("User: \\(user), Path: \\(path)", &custom_vars)
262 .unwrap();
263
264 assert!(result.contains("User:"));
266 assert!(result.contains("Path:"));
267 assert!(!result.contains("\\("));
268 }
269
270 #[test]
271 fn test_duplicate_variables() {
272 let substitutor = VariableSubstitutor::new();
273 let mut custom_vars = HashMap::new();
274 custom_vars.insert("name".to_string(), "Alice".to_string());
275
276 let result = substitutor
277 .substitute("\\(name) and \\(name) again", &custom_vars)
278 .unwrap();
279
280 assert_eq!(result, "Alice and Alice again");
281 }
282
283 #[test]
284 fn test_escaped_backslash() {
285 let substitutor = VariableSubstitutor::new();
286 let custom_vars = HashMap::new();
287
288 let result = substitutor
290 .substitute("Use \\(user) for the username", &custom_vars)
291 .unwrap();
292
293 assert!(!result.contains("\\("));
294 assert!(!result.contains("\\)"));
295 }
296}