use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, HashSet};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum StringSetPatch {
Override(HashSet<String>),
Patch {
#[serde(default)]
r#override: Option<HashSet<String>>,
#[serde(default)]
add: HashSet<String>,
#[serde(default)]
remove: HashSet<String>,
},
}
impl StringSetPatch {
#[must_use]
pub fn override_value(&self) -> Option<&HashSet<String>> {
match self {
Self::Override(v) => Some(v),
Self::Patch { r#override, .. } => r#override.as_ref(),
}
}
#[must_use]
pub fn add_values(&self) -> &HashSet<String> {
static EMPTY: std::sync::LazyLock<HashSet<String>> = std::sync::LazyLock::new(HashSet::new);
match self {
Self::Override(_) => &EMPTY,
Self::Patch { add, .. } => add,
}
}
#[must_use]
pub fn remove_values(&self) -> &HashSet<String> {
static EMPTY: std::sync::LazyLock<HashSet<String>> = std::sync::LazyLock::new(HashSet::new);
match self {
Self::Override(_) => &EMPTY,
Self::Patch { remove, .. } => remove,
}
}
#[must_use]
pub fn has_add_or_remove(&self) -> bool {
!self.add_values().is_empty() || !self.remove_values().is_empty()
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum TargetListPatch {
Override(Vec<String>),
Patch {
#[serde(default)]
r#override: Option<Vec<String>>,
#[serde(default)]
add: Vec<String>,
#[serde(default)]
remove: Vec<String>,
},
}
impl TargetListPatch {
#[must_use]
pub fn override_value(&self) -> Option<&[String]> {
match self {
Self::Override(v) => Some(v),
Self::Patch { r#override, .. } => r#override.as_deref(),
}
}
#[must_use]
pub fn add_values(&self) -> &[String] {
match self {
Self::Override(_) => &[],
Self::Patch { add, .. } => add,
}
}
#[must_use]
pub fn remove_values(&self) -> &[String] {
match self {
Self::Override(_) => &[],
Self::Patch { remove, .. } => remove,
}
}
#[must_use]
pub fn has_add_or_remove(&self) -> bool {
!self.add_values().is_empty() || !self.remove_values().is_empty()
}
}
#[derive(Debug, Clone)]
pub(crate) struct TargetListOps {
override_value: Option<Vec<String>>,
add: Vec<String>,
remove: Vec<String>,
}
impl TargetListOps {
#[must_use]
pub(crate) fn apply_to(&self, base: &[String]) -> Vec<String> {
let mut out = self
.override_value
.clone()
.unwrap_or_else(|| dedup_ordered(base.iter().cloned()));
let remove: HashSet<&str> = self.remove.iter().map(String::as_str).collect();
out.retain(|value| !remove.contains(value.as_str()));
let mut existing: HashSet<String> = out.iter().cloned().collect();
out.extend(
self.add
.iter()
.filter(|value| existing.insert((*value).clone()))
.cloned(),
);
out
}
}
pub(crate) fn combine_target_list_patches<'a>(
name: &str,
source_kind: &str,
patches: impl IntoIterator<Item = (&'a str, &'a TargetListPatch)>,
) -> color_eyre::eyre::Result<Option<TargetListOps>> {
let mut any = false;
let mut override_value: Option<Vec<String>> = None;
let mut add = Vec::new();
let mut remove = Vec::new();
for (expr, patch) in patches {
any = true;
if let Some(value) = patch.override_value() {
let value = dedup_ordered(value.iter().cloned());
match &override_value {
None => override_value = Some(value),
Some(existing) if *existing == value => {}
Some(_) => {
color_eyre::eyre::bail!(
"conflicting overrides for `{name}` from {source_kind} `{expr}`"
);
}
}
}
extend_ordered_unique(&mut add, patch.add_values().iter().cloned());
extend_ordered_unique(&mut remove, patch.remove_values().iter().cloned());
}
Ok(any.then_some(TargetListOps {
override_value,
add,
remove,
}))
}
fn dedup_ordered(values: impl IntoIterator<Item = String>) -> Vec<String> {
let mut seen = HashSet::new();
values
.into_iter()
.filter(|value| seen.insert(value.clone()))
.collect()
}
fn extend_ordered_unique(out: &mut Vec<String>, values: impl IntoIterator<Item = String>) {
let mut seen: HashSet<String> = out.iter().cloned().collect();
out.extend(
values
.into_iter()
.filter(|value| seen.insert(value.clone())),
);
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum FeatureSetVecPatch {
Override(Vec<HashSet<String>>),
Patch {
#[serde(default)]
r#override: Option<Vec<HashSet<String>>>,
#[serde(default)]
add: Vec<HashSet<String>>,
#[serde(default)]
remove: Vec<HashSet<String>>,
},
}
impl FeatureSetVecPatch {
#[must_use]
pub fn override_value(&self) -> Option<&Vec<HashSet<String>>> {
match self {
Self::Override(v) => Some(v),
Self::Patch { r#override, .. } => r#override.as_ref(),
}
}
#[must_use]
pub fn add_values(&self) -> &[HashSet<String>] {
static EMPTY: std::sync::LazyLock<Vec<HashSet<String>>> =
std::sync::LazyLock::new(Vec::new);
match self {
Self::Override(_) => &EMPTY,
Self::Patch { add, .. } => add,
}
}
#[must_use]
pub fn remove_values(&self) -> &[HashSet<String>] {
static EMPTY: std::sync::LazyLock<Vec<HashSet<String>>> =
std::sync::LazyLock::new(Vec::new);
match self {
Self::Override(_) => &EMPTY,
Self::Patch { remove, .. } => remove,
}
}
#[must_use]
pub fn has_add_or_remove(&self) -> bool {
!self.add_values().is_empty() || !self.remove_values().is_empty()
}
}
pub(crate) trait SetPatchInput {
type Elem: Ord + Clone;
fn override_elems(&self) -> Option<BTreeSet<Self::Elem>>;
fn add_elems(&self) -> impl Iterator<Item = Self::Elem> + '_;
fn remove_elems(&self) -> impl Iterator<Item = Self::Elem> + '_;
}
fn normalize_feature_set(set: &HashSet<String>) -> Vec<String> {
let mut v = set.iter().cloned().collect::<Vec<_>>();
v.sort();
v
}
impl SetPatchInput for StringSetPatch {
type Elem = String;
fn override_elems(&self) -> Option<BTreeSet<String>> {
self.override_value().map(|v| v.iter().cloned().collect())
}
fn add_elems(&self) -> impl Iterator<Item = String> + '_ {
self.add_values().iter().cloned()
}
fn remove_elems(&self) -> impl Iterator<Item = String> + '_ {
self.remove_values().iter().cloned()
}
}
impl SetPatchInput for FeatureSetVecPatch {
type Elem = Vec<String>;
fn override_elems(&self) -> Option<BTreeSet<Vec<String>>> {
self.override_value()
.map(|v| v.iter().map(normalize_feature_set).collect())
}
fn add_elems(&self) -> impl Iterator<Item = Vec<String>> + '_ {
self.add_values().iter().map(normalize_feature_set)
}
fn remove_elems(&self) -> impl Iterator<Item = Vec<String>> + '_ {
self.remove_values().iter().map(normalize_feature_set)
}
}
#[derive(Debug, Clone)]
pub(crate) struct SetPatchOps<E: Ord + Clone> {
override_value: Option<BTreeSet<E>>,
add: BTreeSet<E>,
remove: BTreeSet<E>,
}
impl<E: Ord + Clone> SetPatchOps<E> {
pub(crate) fn from_single<P>(patch: &P) -> Self
where
P: SetPatchInput<Elem = E>,
{
Self {
override_value: patch.override_elems(),
add: patch.add_elems().collect(),
remove: patch.remove_elems().collect(),
}
}
fn apply(&self, base: impl FnOnce() -> BTreeSet<E>) -> BTreeSet<E> {
let mut out = match &self.override_value {
Some(value) => value.clone(),
None => base(),
};
for value in &self.remove {
out.remove(value);
}
out.extend(self.add.iter().cloned());
out
}
}
impl SetPatchOps<String> {
#[must_use]
pub(crate) fn apply_to(&self, base: &HashSet<String>) -> HashSet<String> {
self.apply(|| base.iter().cloned().collect())
.into_iter()
.collect()
}
}
impl SetPatchOps<Vec<String>> {
#[must_use]
pub(crate) fn apply_to_feature_sets(&self, base: &[HashSet<String>]) -> Vec<HashSet<String>> {
self.apply(|| base.iter().map(normalize_feature_set).collect())
.into_iter()
.map(|set| set.into_iter().collect())
.collect()
}
}
pub(crate) fn combine_set_patches<'a, P>(
name: &str,
source_kind: &str,
patches: impl IntoIterator<Item = (&'a str, &'a P)>,
) -> color_eyre::eyre::Result<Option<SetPatchOps<P::Elem>>>
where
P: SetPatchInput + 'a,
{
let mut any = false;
let mut override_value: Option<BTreeSet<P::Elem>> = None;
let mut add: BTreeSet<P::Elem> = BTreeSet::new();
let mut remove: BTreeSet<P::Elem> = BTreeSet::new();
for (expr, patch) in patches {
any = true;
if let Some(value) = patch.override_elems() {
match &override_value {
None => override_value = Some(value),
Some(existing) if *existing == value => {}
Some(_) => {
color_eyre::eyre::bail!(
"conflicting overrides for `{name}` from {source_kind} `{expr}`"
);
}
}
}
add.extend(patch.add_elems());
remove.extend(patch.remove_elems());
}
if any {
Ok(Some(SetPatchOps {
override_value,
add,
remove,
}))
} else {
Ok(None)
}
}
#[cfg(test)]
mod test {
use super::{FeatureSetVecPatch, StringSetPatch, TargetListPatch};
use color_eyre::eyre;
use serde_json::json;
use std::collections::HashSet;
#[test]
fn string_set_patch_array_is_override() -> eyre::Result<()> {
let v = json!(["a", "b"]);
let p: StringSetPatch = serde_json::from_value(v)?;
let mut expected: HashSet<String> = HashSet::new();
expected.insert("a".to_string());
expected.insert("b".to_string());
assert_eq!(p.override_value(), Some(&expected));
assert!(p.add_values().is_empty());
assert!(p.remove_values().is_empty());
Ok(())
}
#[test]
fn string_set_patch_object_add_remove() -> eyre::Result<()> {
let v = json!({"add": ["a"], "remove": ["b"]});
let p: StringSetPatch = serde_json::from_value(v)?;
assert!(p.override_value().is_none());
assert!(p.add_values().contains("a"));
assert!(p.remove_values().contains("b"));
Ok(())
}
#[test]
fn feature_set_vec_patch_array_is_override() -> eyre::Result<()> {
let v = json!([["a"], ["b", "c"]]);
let p: FeatureSetVecPatch = serde_json::from_value(v)?;
assert!(p.override_value().is_some());
assert!(p.add_values().is_empty());
assert!(p.remove_values().is_empty());
Ok(())
}
fn hs(values: &[&str]) -> HashSet<String> {
values.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn combine_set_patches_unifies_string_sets() -> eyre::Result<()> {
let base = hs(&["default"]);
let add: StringSetPatch = serde_json::from_value(json!({ "add": ["cuda"] }))?;
let remove: StringSetPatch = serde_json::from_value(json!({ "remove": ["default"] }))?;
let ops = super::combine_set_patches(
"exclude_features",
"target override",
[("cfg(a)", &add), ("cfg(b)", &remove)],
)?
.expect("patches present");
assert_eq!(ops.apply_to(&base), hs(&["cuda"]));
Ok(())
}
#[test]
fn combine_set_patches_unifies_feature_set_lists() -> eyre::Result<()> {
let base = vec![hs(&["a"])];
let add: FeatureSetVecPatch = serde_json::from_value(json!({ "add": [["b", "c"]] }))?;
let ops = super::combine_set_patches(
"include_feature_sets",
"target override",
[("cfg(a)", &add)],
)?
.expect("patch present");
let mut got = ops.apply_to_feature_sets(&base);
got.sort_by_key(super::normalize_feature_set);
assert_eq!(got, vec![hs(&["a"]), hs(&["b", "c"])]);
Ok(())
}
#[test]
fn combine_set_patches_reports_conflicting_overrides() {
let a: StringSetPatch = serde_json::from_value(json!(["x"])).unwrap();
let b: StringSetPatch = serde_json::from_value(json!(["y"])).unwrap();
let err = super::combine_set_patches(
"exclude_features",
"target override",
[("cfg(a)", &a), ("cfg(b)", &b)],
)
.expect_err("conflicting overrides must error");
assert!(err.to_string().contains("conflicting overrides"));
}
#[test]
fn target_list_patch_array_is_ordered_override() -> eyre::Result<()> {
let patch: TargetListPatch = serde_json::from_value(json!(["b", "a", "b"]))?;
let ops = super::combine_target_list_patches("targets", "package config", [("", &patch)])?
.expect("patch present");
assert_eq!(ops.apply_to(&["base".to_string()]), vec!["b", "a"]);
Ok(())
}
#[test]
fn target_list_patch_object_applies_in_declaration_order() -> eyre::Result<()> {
let patch: TargetListPatch =
serde_json::from_value(json!({ "remove": ["base"], "add": ["z", "a", "z"] }))?;
let ops = super::combine_target_list_patches("targets", "package config", [("", &patch)])?
.expect("patch present");
assert_eq!(
ops.apply_to(&["base".to_string(), "kept".to_string()]),
vec!["kept", "z", "a"],
);
Ok(())
}
#[test]
fn target_list_patch_conflicting_overrides_error() -> eyre::Result<()> {
let a: TargetListPatch = serde_json::from_value(json!(["a", "b"]))?;
let b: TargetListPatch = serde_json::from_value(json!(["b", "a"]))?;
let err = super::combine_target_list_patches(
"targets",
"workspace target override",
[("cfg(a)", &a), ("cfg(b)", &b)],
)
.expect_err("ordered overrides differ");
assert!(err.to_string().contains("conflicting overrides"));
Ok(())
}
}