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_.]*)\)").expect(
53 "VariableSubstitutor: snippet variable pattern is valid and should always compile",
54 );
55
56 Self { pattern }
57 }
58
59 pub fn substitute(
68 &self,
69 text: &str,
70 custom_vars: &HashMap<String, String>,
71 ) -> SubstitutionResult<String> {
72 self.substitute_with_session(text, custom_vars, None)
73 }
74
75 pub fn substitute_with_session(
85 &self,
86 text: &str,
87 custom_vars: &HashMap<String, String>,
88 session_vars: Option<&SessionVariables>,
89 ) -> SubstitutionResult<String> {
90 let mut result = text.to_string();
91
92 for cap in self.pattern.captures_iter(text) {
94 let full_match = cap
95 .get(0)
96 .expect("capture group 0 (full match) must be present after a match")
97 .as_str();
98 let var_name = cap
99 .get(1)
100 .expect("capture group 1 (variable name) must be present after a match")
101 .as_str();
102
103 let value = self.resolve_variable_with_session(var_name, custom_vars, session_vars)?;
105
106 result = result.replace(full_match, &value);
108 }
109
110 Ok(result)
111 }
112
113 fn resolve_variable_with_session(
115 &self,
116 name: &str,
117 custom_vars: &HashMap<String, String>,
118 session_vars: Option<&SessionVariables>,
119 ) -> SubstitutionResult<String> {
120 if let Some(value) = custom_vars.get(name) {
122 return Ok(value.clone());
123 }
124
125 if let Some(value) = session_vars.and_then(|session| session.get(name)) {
127 return Ok(value);
128 }
129
130 if let Some(builtin) = BuiltInVariable::parse(name) {
132 return Ok(builtin.resolve());
133 }
134
135 Err(SubstitutionError::UndefinedVariable(name.to_string()))
137 }
138
139 pub fn has_variables(&self, text: &str) -> bool {
141 self.pattern.is_match(text)
142 }
143
144 pub fn extract_variables(&self, text: &str) -> Vec<String> {
146 self.pattern
147 .captures_iter(text)
148 .map(|cap| {
149 cap.get(1)
150 .expect("capture group 1 (variable name) must be present after a match")
151 .as_str()
152 .to_string()
153 })
154 .collect()
155 }
156}
157
158impl Default for VariableSubstitutor {
159 fn default() -> Self {
160 Self::new()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_substitute_builtin_variables() {
170 let substitutor = VariableSubstitutor::new();
171 let custom_vars = HashMap::new();
172
173 let result = substitutor
175 .substitute("Today is \\(date)", &custom_vars)
176 .unwrap();
177 assert!(result.starts_with("Today is "));
178 assert!(!result.contains("\\(date)"));
179
180 let result = substitutor
182 .substitute("User: \\(user)", &custom_vars)
183 .unwrap();
184 assert!(result.starts_with("User: "));
185 assert!(!result.contains("\\(user)"));
186 }
187
188 #[test]
189 fn test_substitute_custom_variables() {
190 let substitutor = VariableSubstitutor::new();
191 let mut custom_vars = HashMap::new();
192 custom_vars.insert("name".to_string(), "Alice".to_string());
193 custom_vars.insert("project".to_string(), "par-term".to_string());
194
195 let result = substitutor
196 .substitute("Hello \\(name), welcome to \\(project)!", &custom_vars)
197 .unwrap();
198
199 assert_eq!(result, "Hello Alice, welcome to par-term!");
200 }
201
202 #[test]
203 fn test_substitute_mixed_variables() {
204 let substitutor = VariableSubstitutor::new();
205 let mut custom_vars = HashMap::new();
206 custom_vars.insert("greeting".to_string(), "Hello".to_string());
207
208 let result = substitutor
209 .substitute("\\(greeting) \\(user), today is \\(date)", &custom_vars)
210 .unwrap();
211
212 assert!(result.starts_with("Hello "));
213 assert!(!result.contains("\\("));
214 }
215
216 #[test]
217 fn test_undefined_variable() {
218 let substitutor = VariableSubstitutor::new();
219 let custom_vars = HashMap::new();
220
221 let result = substitutor.substitute("Value: \\(undefined)", &custom_vars);
222
223 assert!(result.is_err());
224 match result.unwrap_err() {
225 SubstitutionError::UndefinedVariable(name) => assert_eq!(name, "undefined"),
226 _ => panic!("Expected UndefinedVariable error"),
227 }
228 }
229
230 #[test]
231 fn test_has_variables() {
232 let substitutor = VariableSubstitutor::new();
233
234 assert!(substitutor.has_variables("Hello \\(user)"));
235 assert!(!substitutor.has_variables("Hello world"));
236 }
237
238 #[test]
239 fn test_extract_variables() {
240 let substitutor = VariableSubstitutor::new();
241
242 let vars = substitutor.extract_variables("Hello \\(user), today is \\(date)");
243 assert_eq!(vars, vec!["user", "date"]);
244 }
245
246 #[test]
247 fn test_no_variables() {
248 let substitutor = VariableSubstitutor::new();
249 let custom_vars = HashMap::new();
250
251 let result = substitutor
252 .substitute("Just plain text with no variables", &custom_vars)
253 .unwrap();
254
255 assert_eq!(result, "Just plain text with no variables");
256 }
257
258 #[test]
259 fn test_empty_custom_vars() {
260 let substitutor = VariableSubstitutor::new();
261 let custom_vars = HashMap::new();
262
263 let result = substitutor
264 .substitute("User: \\(user), Path: \\(path)", &custom_vars)
265 .unwrap();
266
267 assert!(result.contains("User:"));
269 assert!(result.contains("Path:"));
270 assert!(!result.contains("\\("));
271 }
272
273 #[test]
274 fn test_duplicate_variables() {
275 let substitutor = VariableSubstitutor::new();
276 let mut custom_vars = HashMap::new();
277 custom_vars.insert("name".to_string(), "Alice".to_string());
278
279 let result = substitutor
280 .substitute("\\(name) and \\(name) again", &custom_vars)
281 .unwrap();
282
283 assert_eq!(result, "Alice and Alice again");
284 }
285
286 #[test]
287 fn test_escaped_backslash() {
288 let substitutor = VariableSubstitutor::new();
289 let custom_vars = HashMap::new();
290
291 let result = substitutor
293 .substitute("Use \\(user) for the username", &custom_vars)
294 .unwrap();
295
296 assert!(!result.contains("\\("));
297 assert!(!result.contains("\\)"));
298 }
299}