use super::flags::FlagConfig;
use super::patch::{FeatureSetVecPatch, StringSetPatch, TargetListPatch};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashSet};
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct ScopeConfig {
#[serde(default)]
pub replace: bool,
#[serde(default)]
pub driver: Option<String>,
#[serde(default)]
pub expand_targets: Option<bool>,
#[serde(default)]
pub targets: Option<TargetListPatch>,
#[serde(default)]
pub exclude_packages: Option<StringSetPatch>,
#[serde(default, flatten)]
pub features: FeatureMatrixPatch,
#[serde(default, flatten)]
pub flags: FlagConfig,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct SectionConfig {
#[serde(flatten)]
pub settings: ScopeConfig,
#[serde(default, rename = "subcommands")]
pub subcommands: BTreeMap<String, ScopeConfig>,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct RootConfig {
#[serde(flatten)]
pub base: SectionConfig,
#[serde(default, rename = "target")]
pub targets: BTreeMap<String, SectionConfig>,
#[serde(flatten)]
pub(crate) deprecated: DeprecatedTomlKeys,
}
pub type Config = RootConfig;
pub type WorkspaceConfig = RootConfig;
pub type TargetOverride = SectionConfig;
pub type WorkspaceTargetOverride = SectionConfig;
pub type CommandCapabilities = ScopeConfig;
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct FeatureMatrixPatch {
#[serde(default)]
pub isolated_feature_sets: Option<FeatureSetVecPatch>,
#[serde(default)]
pub exclude_features: Option<StringSetPatch>,
#[serde(default)]
pub include_features: Option<StringSetPatch>,
#[serde(default)]
pub only_features: Option<StringSetPatch>,
#[serde(default)]
pub skip_optional_dependencies: Option<bool>,
#[serde(default)]
pub exclude_feature_sets: Option<FeatureSetVecPatch>,
#[serde(default)]
pub include_feature_sets: Option<FeatureSetVecPatch>,
#[serde(default)]
pub allow_feature_sets: Option<FeatureSetVecPatch>,
#[serde(default)]
pub no_empty_feature_set: Option<bool>,
#[serde(default)]
pub matrix: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(default)]
pub max_combinations: Option<u128>,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub(crate) struct DeprecatedTomlKeys {
#[serde(default)]
pub skip_feature_sets: Vec<HashSet<String>>,
#[serde(default)]
pub denylist: HashSet<String>,
#[serde(default)]
pub exact_combinations: Vec<HashSet<String>>,
}
#[cfg(test)]
mod tests {
use super::{FeatureMatrixPatch, ScopeConfig, SectionConfig};
use crate::config::FlagConfig;
use crate::config::patch::{StringSetPatch, TargetListPatch};
use serde::Serialize;
use serde_json::Value;
use std::collections::BTreeSet;
#[test]
fn scope_config_splits_flattened_feature_and_flag_keys() {
let value = serde_json::json!({
"replace": true,
"exclude_features": ["gpu"],
"only_features": { "add": ["core"] },
"skip_optional_dependencies": true,
"matrix": { "kind": "ci" },
"pedantic": true,
});
let scope: ScopeConfig = serde_json::from_value(value).expect("deserialize ScopeConfig");
assert!(scope.replace);
assert_eq!(scope.flags.pedantic, Some(true));
assert_eq!(scope.features.skip_optional_dependencies, Some(true));
assert!(matches!(
scope.features.exclude_features,
Some(StringSetPatch::Override(_))
));
assert!(matches!(
scope.features.only_features,
Some(StringSetPatch::Patch { .. })
));
assert!(scope.features.matrix.is_some());
}
#[test]
fn section_config_splits_subcommands_from_settings() {
let value = serde_json::json!({
"targets": ["wasm", "linux"],
"subcommands": {
"test": { "expand_targets": false, "verbose": true },
},
});
let section: SectionConfig =
serde_json::from_value(value).expect("deserialize SectionConfig");
assert!(matches!(
section.settings.targets,
Some(TargetListPatch::Override(_))
));
let command = section.subcommands.get("test").expect("test command");
assert_eq!(command.expand_targets, Some(false));
assert_eq!(command.flags.verbose, Some(true));
}
#[test]
fn absent_keys_default_to_none() {
let scope: ScopeConfig =
serde_json::from_value(serde_json::json!({})).expect("deserialize empty");
assert!(scope.expand_targets.is_none());
assert!(scope.features.exclude_features.is_none());
assert!(scope.features.skip_optional_dependencies.is_none());
}
#[test]
fn empty_override_array_is_a_replace_not_absent() {
let scope: ScopeConfig = serde_json::from_value(serde_json::json!({ "only_features": [] }))
.expect("deserialize empty override");
match scope.features.only_features {
Some(StringSetPatch::Override(set)) => assert!(set.is_empty()),
other => panic!("expected empty override, got {other:?}"),
}
}
#[test]
fn flattened_schema_key_sets_are_disjoint() {
let scope = [
"replace",
"driver",
"expand_targets",
"targets",
"exclude_packages",
]
.into_iter()
.map(String::from)
.collect::<BTreeSet<_>>();
let features = keys_for(FeatureMatrixPatch::default());
let flags = keys_for(FlagConfig::default());
assert_disjoint("scope", &scope, "features", &features);
assert_disjoint("scope", &scope, "flags", &flags);
assert_disjoint("features", &features, "flags", &flags);
}
fn keys_for<T: Serialize>(value: T) -> BTreeSet<String> {
match serde_json::to_value(value).expect("serialize default") {
Value::Object(map) => map.keys().cloned().collect(),
other => panic!("expected object, got {other:?}"),
}
}
fn assert_disjoint(a_name: &str, a: &BTreeSet<String>, b_name: &str, b: &BTreeSet<String>) {
let overlap = a.intersection(b).collect::<Vec<_>>();
assert!(overlap.is_empty(), "{a_name}/{b_name} overlap: {overlap:?}");
}
}