1use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use serde_yaml::Value;
6use std::collections::HashMap;
7use thiserror::Error;
8
9#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct MatrixConfig {
11 #[serde(flatten)]
12 pub parameters: IndexMap<String, Value>,
13 #[serde(default)]
14 pub include: Vec<HashMap<String, Value>>,
15 #[serde(default)]
16 pub exclude: Vec<HashMap<String, Value>>,
17 #[serde(default, rename = "max-parallel")]
18 pub max_parallel: Option<usize>,
19 #[serde(default, rename = "fail-fast")]
20 pub fail_fast: Option<bool>,
21}
22
23impl Default for MatrixConfig {
24 fn default() -> Self {
25 Self {
26 parameters: IndexMap::new(),
27 include: Vec::new(),
28 exclude: Vec::new(),
29 max_parallel: None,
30 fail_fast: Some(true),
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq)]
36pub struct MatrixCombination {
37 pub values: HashMap<String, Value>,
38 pub is_included: bool, }
40
41impl MatrixCombination {
42 pub fn new(values: HashMap<String, Value>) -> Self {
43 Self {
44 values,
45 is_included: false,
46 }
47 }
48
49 pub fn from_include(values: HashMap<String, Value>) -> Self {
50 Self {
51 values,
52 is_included: true,
53 }
54 }
55}
56
57#[derive(Error, Debug)]
58pub enum MatrixError {
59 #[error("Invalid matrix parameter format: {0}")]
60 InvalidParameterFormat(String),
61
62 #[error("Failed to expand matrix: {0}")]
63 ExpansionError(String),
64}
65
66pub fn expand_matrix(matrix: &MatrixConfig) -> Result<Vec<MatrixCombination>, MatrixError> {
68 let mut combinations = Vec::new();
69
70 let param_combinations = generate_base_combinations(matrix)?;
72
73 let filtered_combinations = apply_exclude_filters(param_combinations, &matrix.exclude);
75 combinations.extend(filtered_combinations);
76
77 for include_item in &matrix.include {
79 combinations.push(MatrixCombination::from_include(include_item.clone()));
80 }
81
82 if combinations.is_empty() {
83 return Err(MatrixError::ExpansionError(
84 "No valid combinations found after applying filters".to_string(),
85 ));
86 }
87
88 Ok(combinations)
89}
90
91fn generate_base_combinations(
93 matrix: &MatrixConfig,
94) -> Result<Vec<MatrixCombination>, MatrixError> {
95 let mut param_arrays: IndexMap<String, Vec<Value>> = IndexMap::new();
97
98 for (param_name, param_value) in &matrix.parameters {
99 match param_value {
100 Value::Sequence(array) => {
101 param_arrays.insert(param_name.clone(), array.clone());
102 }
103 _ => {
104 let single_value = vec![param_value.clone()];
106 param_arrays.insert(param_name.clone(), single_value);
107 }
108 }
109 }
110
111 if param_arrays.is_empty() {
112 return Err(MatrixError::InvalidParameterFormat(
113 "Matrix has no valid parameters".to_string(),
114 ));
115 }
116
117 let param_names: Vec<String> = param_arrays.keys().cloned().collect();
119 let param_values: Vec<Vec<Value>> = param_arrays.values().cloned().collect();
120
121 let combinations = if !param_values.is_empty() {
123 generate_combinations(¶m_names, ¶m_values, 0, &mut HashMap::new())?
124 } else {
125 vec![]
126 };
127
128 Ok(combinations)
129}
130
131fn generate_combinations(
133 param_names: &[String],
134 param_values: &[Vec<Value>],
135 current_depth: usize,
136 current_combination: &mut HashMap<String, Value>,
137) -> Result<Vec<MatrixCombination>, MatrixError> {
138 if current_depth == param_names.len() {
139 return Ok(vec![MatrixCombination::new(current_combination.clone())]);
141 }
142
143 let mut result = Vec::new();
144 let param_name = ¶m_names[current_depth];
145 let values = ¶m_values[current_depth];
146
147 for value in values {
148 current_combination.insert(param_name.clone(), value.clone());
149
150 let mut new_combinations = generate_combinations(
151 param_names,
152 param_values,
153 current_depth + 1,
154 current_combination,
155 )?;
156
157 result.append(&mut new_combinations);
158 }
159
160 current_combination.remove(param_name);
162
163 Ok(result)
164}
165
166fn apply_exclude_filters(
168 combinations: Vec<MatrixCombination>,
169 exclude_patterns: &[HashMap<String, Value>],
170) -> Vec<MatrixCombination> {
171 if exclude_patterns.is_empty() {
172 return combinations;
173 }
174
175 combinations
176 .into_iter()
177 .filter(|combination| !is_excluded(combination, exclude_patterns))
178 .collect()
179}
180
181fn is_excluded(
183 combination: &MatrixCombination,
184 exclude_patterns: &[HashMap<String, Value>],
185) -> bool {
186 for exclude in exclude_patterns {
187 let mut excluded = true;
188
189 for (key, value) in exclude {
190 match combination.values.get(key) {
191 Some(combo_value) if combo_value == value => {
192 continue;
194 }
195 _ => {
196 excluded = false;
198 break;
199 }
200 }
201 }
202
203 if excluded {
204 return true;
205 }
206 }
207
208 false
209}
210
211pub fn format_combination_name(job_name: &str, combination: &MatrixCombination) -> String {
213 let params = combination
214 .values
215 .iter()
216 .map(|(k, v)| format!("{}: {}", k, value_to_string(v)))
217 .collect::<Vec<_>>()
218 .join(", ");
219
220 format!("{} ({})", job_name, params)
221}
222
223fn value_to_string(value: &Value) -> String {
225 match value {
226 Value::String(s) => s.clone(),
227 Value::Number(n) => n.to_string(),
228 Value::Bool(b) => b.to_string(),
229 Value::Sequence(seq) => {
230 let items = seq
231 .iter()
232 .map(value_to_string)
233 .collect::<Vec<_>>()
234 .join(", ");
235 format!("[{}]", items)
236 }
237 Value::Mapping(map) => {
238 let items = map
239 .iter()
240 .map(|(k, v)| format!("{}: {}", value_to_string(k), value_to_string(v)))
241 .collect::<Vec<_>>()
242 .join(", ");
243 format!("{{{}}}", items)
244 }
245 Value::Null => "null".to_string(),
246 _ => "unknown".to_string(),
247 }
248}