use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MatrixConfig {
#[serde(flatten)]
pub parameters: IndexMap<String, Value>,
#[serde(default)]
pub include: Vec<HashMap<String, Value>>,
#[serde(default)]
pub exclude: Vec<HashMap<String, Value>>,
#[serde(default, rename = "max-parallel")]
pub max_parallel: Option<usize>,
#[serde(default, rename = "fail-fast")]
pub fail_fast: Option<bool>,
}
impl Default for MatrixConfig {
fn default() -> Self {
Self {
parameters: IndexMap::new(),
include: Vec::new(),
exclude: Vec::new(),
max_parallel: None,
fail_fast: Some(true),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatrixCombination {
pub values: HashMap<String, Value>,
pub is_included: bool, }
impl MatrixCombination {
pub fn new(values: HashMap<String, Value>) -> Self {
Self {
values,
is_included: false,
}
}
pub fn from_include(values: HashMap<String, Value>) -> Self {
Self {
values,
is_included: true,
}
}
}
#[derive(Error, Debug)]
pub enum MatrixError {
#[error("Invalid matrix parameter format: {0}")]
InvalidParameterFormat(String),
#[error("Failed to expand matrix: {0}")]
ExpansionError(String),
}
pub fn expand_matrix(matrix: &MatrixConfig) -> Result<Vec<MatrixCombination>, MatrixError> {
let mut combinations = Vec::new();
let param_combinations = generate_base_combinations(matrix)?;
let filtered_combinations = apply_exclude_filters(param_combinations, &matrix.exclude);
combinations.extend(filtered_combinations);
for include_item in &matrix.include {
combinations.push(MatrixCombination::from_include(include_item.clone()));
}
if combinations.is_empty() {
return Err(MatrixError::ExpansionError("No valid combinations found after applying filters".to_string()));
}
Ok(combinations)
}
fn generate_base_combinations(
matrix: &MatrixConfig,
) -> Result<Vec<MatrixCombination>, MatrixError> {
let mut param_arrays: IndexMap<String, Vec<Value>> = IndexMap::new();
for (param_name, param_value) in &matrix.parameters {
match param_value {
Value::Sequence(array) => {
param_arrays.insert(param_name.clone(), array.clone());
}
_ => {
let single_value = vec![param_value.clone()];
param_arrays.insert(param_name.clone(), single_value);
}
}
}
if param_arrays.is_empty() {
return Err(MatrixError::InvalidParameterFormat("Matrix has no valid parameters".to_string()));
}
let param_names: Vec<String> = param_arrays.keys().cloned().collect();
let param_values: Vec<Vec<Value>> = param_arrays.values().cloned().collect();
let combinations = if !param_values.is_empty() {
generate_combinations(¶m_names, ¶m_values, 0, &mut HashMap::new())?
} else {
vec![]
};
Ok(combinations)
}
fn generate_combinations(
param_names: &[String],
param_values: &[Vec<Value>],
current_depth: usize,
current_combination: &mut HashMap<String, Value>,
) -> Result<Vec<MatrixCombination>, MatrixError> {
if current_depth == param_names.len() {
return Ok(vec![MatrixCombination::new(current_combination.clone())]);
}
let mut result = Vec::new();
let param_name = ¶m_names[current_depth];
let values = ¶m_values[current_depth];
for value in values {
current_combination.insert(param_name.clone(), value.clone());
let mut new_combinations = generate_combinations(
param_names,
param_values,
current_depth + 1,
current_combination,
)?;
result.append(&mut new_combinations);
}
current_combination.remove(param_name);
Ok(result)
}
fn apply_exclude_filters(
combinations: Vec<MatrixCombination>,
exclude_patterns: &[HashMap<String, Value>],
) -> Vec<MatrixCombination> {
if exclude_patterns.is_empty() {
return combinations;
}
combinations
.into_iter()
.filter(|combination| {
!is_excluded(combination, exclude_patterns)
})
.collect()
}
fn is_excluded(
combination: &MatrixCombination,
exclude_patterns: &[HashMap<String, Value>],
) -> bool {
for exclude in exclude_patterns {
let mut excluded = true;
for (key, value) in exclude {
match combination.values.get(key) {
Some(combo_value) if combo_value == value => {
continue;
}
_ => {
excluded = false;
break;
}
}
}
if excluded {
return true;
}
}
false
}
pub fn format_combination_name(job_name: &str, combination: &MatrixCombination) -> String {
let params = combination
.values
.iter()
.map(|(k, v)| format!("{}: {}", k, value_to_string(v)))
.collect::<Vec<_>>()
.join(", ");
format!("{} ({})", job_name, params)
}
fn value_to_string(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Sequence(seq) => {
let items = seq
.iter()
.map(value_to_string)
.collect::<Vec<_>>()
.join(", ");
format!("[{}]", items)
}
Value::Mapping(map) => {
let items = map
.iter()
.map(|(k, v)| format!("{}: {}", value_to_string(k), value_to_string(v)))
.collect::<Vec<_>>()
.join(", ");
format!("{{{}}}", items)
}
Value::Null => "null".to_string(),
_ => "unknown".to_string(),
}
}