rust_actions/
matrix.rs

1use crate::parser::{Matrix, Strategy};
2use serde_json::Value;
3use std::collections::HashMap;
4
5pub type MatrixCombination = HashMap<String, Value>;
6
7pub fn expand_matrix(strategy: &Strategy) -> Vec<MatrixCombination> {
8    expand_matrix_inner(&strategy.matrix)
9}
10
11pub fn expand_matrix_inner(matrix: &Matrix) -> Vec<MatrixCombination> {
12    if matrix.dimensions.is_empty() && matrix.include.is_empty() {
13        return vec![HashMap::new()];
14    }
15
16    let mut combinations = cartesian_product(&matrix.dimensions);
17
18    combinations.retain(|combo| !matches_any_exclude(combo, &matrix.exclude));
19
20    for include in &matrix.include {
21        let mut new_combo = HashMap::new();
22        for (key, value) in include {
23            new_combo.insert(key.clone(), value.clone());
24        }
25        combinations.push(new_combo);
26    }
27
28    if combinations.is_empty() {
29        vec![HashMap::new()]
30    } else {
31        combinations
32    }
33}
34
35fn cartesian_product(matrix: &HashMap<String, Vec<Value>>) -> Vec<MatrixCombination> {
36    if matrix.is_empty() {
37        return vec![];
38    }
39
40    let keys: Vec<&String> = matrix.keys().collect();
41    let mut result = vec![HashMap::new()];
42
43    for key in keys {
44        let values = &matrix[key];
45        let mut new_result = Vec::new();
46
47        for combo in &result {
48            for value in values {
49                let mut new_combo = combo.clone();
50                new_combo.insert(key.clone(), value.clone());
51                new_result.push(new_combo);
52            }
53        }
54
55        result = new_result;
56    }
57
58    result
59}
60
61fn matches_any_exclude(combo: &MatrixCombination, excludes: &[HashMap<String, Value>]) -> bool {
62    excludes.iter().any(|exclude| matches_exclude(combo, exclude))
63}
64
65fn matches_exclude(combo: &MatrixCombination, exclude: &HashMap<String, Value>) -> bool {
66    exclude.iter().all(|(key, value)| {
67        combo
68            .get(key)
69            .map(|v| values_equal(v, value))
70            .unwrap_or(false)
71    })
72}
73
74fn values_equal(a: &Value, b: &Value) -> bool {
75    match (a, b) {
76        (Value::String(a), Value::String(b)) => a == b,
77        (Value::Number(a), Value::Number(b)) => a.as_f64() == b.as_f64(),
78        (Value::Bool(a), Value::Bool(b)) => a == b,
79        (Value::Null, Value::Null) => true,
80        _ => a == b,
81    }
82}
83
84pub fn format_matrix_suffix(combo: &MatrixCombination) -> String {
85    if combo.is_empty() {
86        return String::new();
87    }
88
89    let mut parts: Vec<String> = combo
90        .iter()
91        .map(|(k, v)| format!("{}={}", k, format_value(v)))
92        .collect();
93    parts.sort();
94
95    format!(" [{}]", parts.join(", "))
96}
97
98fn format_value(value: &Value) -> String {
99    match value {
100        Value::String(s) => s.clone(),
101        Value::Number(n) => n.to_string(),
102        Value::Bool(b) => b.to_string(),
103        Value::Null => "null".to_string(),
104        _ => value.to_string(),
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use serde_json::json;
112
113    #[test]
114    fn test_empty_matrix() {
115        let matrix = Matrix {
116            dimensions: HashMap::new(),
117            include: vec![],
118            exclude: vec![],
119        };
120
121        let combos = expand_matrix_inner(&matrix);
122        assert_eq!(combos.len(), 1);
123        assert!(combos[0].is_empty());
124    }
125
126    #[test]
127    fn test_single_dimension_matrix() {
128        let mut dimensions = HashMap::new();
129        dimensions.insert("version".to_string(), vec![json!("v1"), json!("v2")]);
130
131        let matrix = Matrix {
132            dimensions,
133            include: vec![],
134            exclude: vec![],
135        };
136
137        let combos = expand_matrix_inner(&matrix);
138        assert_eq!(combos.len(), 2);
139    }
140
141    #[test]
142    fn test_cartesian_product() {
143        let mut dimensions = HashMap::new();
144        dimensions.insert("a".to_string(), vec![json!(true), json!(false)]);
145        dimensions.insert("b".to_string(), vec![json!(true), json!(false)]);
146
147        let matrix = Matrix {
148            dimensions,
149            include: vec![],
150            exclude: vec![],
151        };
152
153        let combos = expand_matrix_inner(&matrix);
154        assert_eq!(combos.len(), 4);
155    }
156
157    #[test]
158    fn test_exclude() {
159        let mut dimensions = HashMap::new();
160        dimensions.insert("a".to_string(), vec![json!("v1"), json!("v2")]);
161        dimensions.insert("b".to_string(), vec![json!("v1"), json!("v2")]);
162
163        let mut exclude = HashMap::new();
164        exclude.insert("a".to_string(), json!("v1"));
165        exclude.insert("b".to_string(), json!("v2"));
166
167        let matrix = Matrix {
168            dimensions,
169            include: vec![],
170            exclude: vec![exclude],
171        };
172
173        let combos = expand_matrix_inner(&matrix);
174        assert_eq!(combos.len(), 3);
175
176        let excluded_combo: MatrixCombination =
177            [("a".to_string(), json!("v1")), ("b".to_string(), json!("v2"))]
178                .into_iter()
179                .collect();
180
181        assert!(!combos.contains(&excluded_combo));
182    }
183
184    #[test]
185    fn test_include() {
186        let mut dimensions = HashMap::new();
187        dimensions.insert("a".to_string(), vec![json!("v1")]);
188
189        let mut include = HashMap::new();
190        include.insert("a".to_string(), json!("v3-beta"));
191        include.insert("experimental".to_string(), json!(true));
192
193        let matrix = Matrix {
194            dimensions,
195            include: vec![include],
196            exclude: vec![],
197        };
198
199        let combos = expand_matrix_inner(&matrix);
200        assert_eq!(combos.len(), 2);
201
202        let has_beta = combos
203            .iter()
204            .any(|c| c.get("a") == Some(&json!("v3-beta")));
205        assert!(has_beta);
206
207        let has_experimental = combos.iter().any(|c| c.get("experimental").is_some());
208        assert!(has_experimental);
209    }
210
211    #[test]
212    fn test_format_matrix_suffix() {
213        let combo: MatrixCombination = [
214            ("feature_x".to_string(), json!(true)),
215            ("feature_y".to_string(), json!(false)),
216        ]
217        .into_iter()
218        .collect();
219
220        let suffix = format_matrix_suffix(&combo);
221        assert!(suffix.contains("feature_x=true"));
222        assert!(suffix.contains("feature_y=false"));
223    }
224
225    #[test]
226    fn test_format_empty_matrix() {
227        let combo: MatrixCombination = HashMap::new();
228        let suffix = format_matrix_suffix(&combo);
229        assert!(suffix.is_empty());
230    }
231}