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}