Skip to main content

wrkflw_executor/
substitution.rs

1use lazy_static::lazy_static;
2use regex::Regex;
3use serde_yaml::Value;
4use std::collections::HashMap;
5
6lazy_static! {
7    static ref MATRIX_PATTERN: Regex =
8        Regex::new(r"\$\{\{\s*matrix\.([a-zA-Z0-9_]+)\s*\}\}").unwrap();
9}
10
11/// Preprocesses a command string to replace GitHub-style matrix variable references
12/// with their values from the environment
13#[allow(dead_code)]
14pub fn preprocess_command(command: &str, matrix_values: &HashMap<String, Value>) -> String {
15    // Replace matrix references like ${{ matrix.os }} with their values
16    let result = MATRIX_PATTERN.replace_all(command, |caps: &regex::Captures| {
17        let var_name = &caps[1];
18
19        // Get the value from matrix context
20        if let Some(value) = matrix_values.get(var_name) {
21            // Convert value to string
22            match value {
23                Value::String(s) => s.clone(),
24                Value::Number(n) => n.to_string(),
25                Value::Bool(b) => b.to_string(),
26                _ => format!("\\${{{{ matrix.{} }}}}", var_name), // Escape $ for shell
27            }
28        } else {
29            // Keep original if not found but escape $ to prevent shell errors
30            format!("\\${{{{ matrix.{} }}}}", var_name)
31        }
32    });
33
34    result.into_owned()
35}
36
37/// Apply variable substitution to step run commands
38#[allow(dead_code)]
39pub fn process_step_run(run: &str, matrix_combination: &Option<HashMap<String, Value>>) -> String {
40    if let Some(matrix) = matrix_combination {
41        preprocess_command(run, matrix)
42    } else {
43        // Escape $ in GitHub expression syntax to prevent shell interpretation
44        MATRIX_PATTERN
45            .replace_all(run, |caps: &regex::Captures| {
46                let var_name = &caps[1];
47                format!("\\${{{{ matrix.{} }}}}", var_name)
48            })
49            .to_string()
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn test_preprocess_simple_matrix_vars() {
59        let mut matrix = HashMap::new();
60        matrix.insert("os".to_string(), Value::String("ubuntu-latest".to_string()));
61        matrix.insert(
62            "node".to_string(),
63            Value::Number(serde_yaml::Number::from(14)),
64        );
65
66        let cmd = "echo \"Running on ${{ matrix.os }} with Node ${{ matrix.node }}\"";
67        let processed = preprocess_command(cmd, &matrix);
68
69        assert_eq!(processed, "echo \"Running on ubuntu-latest with Node 14\"");
70    }
71
72    #[test]
73    fn test_preprocess_with_missing_vars() {
74        let mut matrix = HashMap::new();
75        matrix.insert("os".to_string(), Value::String("ubuntu-latest".to_string()));
76
77        let cmd = "echo \"Running on ${{ matrix.os }} with Node ${{ matrix.node }}\"";
78        let processed = preprocess_command(cmd, &matrix);
79
80        // Missing vars should be escaped
81        assert_eq!(
82            processed,
83            "echo \"Running on ubuntu-latest with Node \\${{ matrix.node }}\""
84        );
85    }
86
87    #[test]
88    fn test_preprocess_preserves_other_text() {
89        let mut matrix = HashMap::new();
90        matrix.insert("os".to_string(), Value::String("ubuntu-latest".to_string()));
91
92        let cmd = "echo \"Starting job\" && echo \"OS: ${{ matrix.os }}\" && echo \"Done!\"";
93        let processed = preprocess_command(cmd, &matrix);
94
95        assert_eq!(
96            processed,
97            "echo \"Starting job\" && echo \"OS: ubuntu-latest\" && echo \"Done!\""
98        );
99    }
100
101    #[test]
102    fn test_process_without_matrix() {
103        let cmd = "echo \"Value: ${{ matrix.value }}\"";
104        let processed = process_step_run(cmd, &None);
105
106        assert_eq!(processed, "echo \"Value: \\${{ matrix.value }}\"");
107    }
108}