use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub(crate) struct StringSetPatchOps {
override_value: Option<HashSet<String>>,
add: HashSet<String>,
remove: HashSet<String>,
}
impl StringSetPatchOps {
#[must_use]
pub(crate) fn apply_to(&self, base: &HashSet<String>) -> HashSet<String> {
let mut out = if let Some(value) = &self.override_value {
value.clone()
} else {
base.clone()
};
for value in &self.remove {
out.remove(value);
}
out.extend(self.add.iter().cloned());
out
}
}
#[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()
}
}
pub(crate) fn combine_string_set_patches<'a>(
name: &str,
source_kind: &str,
patches: impl IntoIterator<Item = (&'a str, &'a StringSetPatch)>,
) -> color_eyre::eyre::Result<Option<StringSetPatchOps>> {
let mut any = false;
let mut override_value: Option<HashSet<String>> = None;
let mut add: HashSet<String> = HashSet::new();
let mut remove: HashSet<String> = HashSet::new();
for (expr, patch) in patches {
any = true;
if let Some(value) = patch.override_value() {
match &override_value {
None => override_value = Some(value.clone()),
Some(existing) if existing == value => {}
Some(_) => {
color_eyre::eyre::bail!(
"conflicting overrides for `{name}` from {source_kind} `{expr}`"
);
}
}
}
add.extend(patch.add_values().iter().cloned());
remove.extend(patch.remove_values().iter().cloned());
}
if any {
Ok(Some(StringSetPatchOps {
override_value,
add,
remove,
}))
} else {
Ok(None)
}
}
#[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()
}
}
#[cfg(test)]
mod test {
use super::{FeatureSetVecPatch, StringSetPatch};
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(())
}
}