use std::collections::BTreeMap;
use zeroize::Zeroizing;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeType {
Added,
Removed,
Modified,
}
#[derive(Debug)]
pub struct DiffEntry {
pub key: String,
pub change: ChangeType,
pub old_value: Option<Zeroizing<String>>,
pub new_value: Option<Zeroizing<String>>,
}
#[derive(Debug)]
pub struct DiffReport {
pub env_name: String,
pub entries: Vec<DiffEntry>,
pub added: usize,
pub removed: usize,
pub modified: usize,
}
impl DiffReport {
pub fn has_differences(&self) -> bool {
!self.entries.is_empty()
}
pub fn total(&self) -> usize {
self.added + self.removed + self.modified
}
}
pub fn compute_diff(
env_name: &str,
vault_secrets: BTreeMap<String, Zeroizing<String>>,
artifact_secrets: BTreeMap<String, Zeroizing<String>>,
) -> DiffReport {
let mut entries = Vec::new();
let mut added: usize = 0;
let mut removed: usize = 0;
let mut modified: usize = 0;
let mut vault_iter = vault_secrets.into_iter().peekable();
let mut artifact_iter = artifact_secrets.into_iter().peekable();
loop {
match (vault_iter.peek(), artifact_iter.peek()) {
(Some((vk, _)), Some((ak, _))) => {
match vk.cmp(ak) {
std::cmp::Ordering::Less => {
let (key, value) = vault_iter.next().unwrap();
entries.push(DiffEntry {
key,
change: ChangeType::Added,
old_value: None,
new_value: Some(value),
});
added += 1;
}
std::cmp::Ordering::Greater => {
let (key, value) = artifact_iter.next().unwrap();
entries.push(DiffEntry {
key,
change: ChangeType::Removed,
old_value: Some(value),
new_value: None,
});
removed += 1;
}
std::cmp::Ordering::Equal => {
let (key, new_val) = vault_iter.next().unwrap();
let (_, old_val) = artifact_iter.next().unwrap();
if *new_val != *old_val {
entries.push(DiffEntry {
key,
change: ChangeType::Modified,
old_value: Some(old_val),
new_value: Some(new_val),
});
modified += 1;
}
}
}
}
(Some(_), None) => {
let (key, value) = vault_iter.next().unwrap();
entries.push(DiffEntry {
key,
change: ChangeType::Added,
old_value: None,
new_value: Some(value),
});
added += 1;
}
(None, Some(_)) => {
let (key, value) = artifact_iter.next().unwrap();
entries.push(DiffEntry {
key,
change: ChangeType::Removed,
old_value: Some(value),
new_value: None,
});
removed += 1;
}
(None, None) => break,
}
}
DiffReport {
env_name: env_name.to_string(),
entries,
added,
removed,
modified,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn secrets(pairs: &[(&str, &str)]) -> BTreeMap<String, Zeroizing<String>> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), Zeroizing::new(v.to_string())))
.collect()
}
#[test]
fn diff_all_added() {
let vault = secrets(&[("A", "a"), ("B", "b")]);
let artifact = BTreeMap::new();
let report = compute_diff("development", vault, artifact);
assert_eq!(report.entries.len(), 2, "expected 2 Added entries");
assert!(report.has_differences());
assert_eq!(report.added, 2);
assert_eq!(report.removed, 0);
assert_eq!(report.modified, 0);
assert_eq!(report.entries[0].key, "A");
assert_eq!(report.entries[1].key, "B");
assert_eq!(report.entries[0].change, ChangeType::Added);
assert_eq!(report.entries[1].change, ChangeType::Added);
}
#[test]
fn diff_all_removed() {
let vault = BTreeMap::new();
let artifact = secrets(&[("X", "x"), ("Y", "y")]);
let report = compute_diff("development", vault, artifact);
assert_eq!(report.entries.len(), 2, "expected 2 Removed entries");
assert!(report.has_differences());
assert_eq!(report.removed, 2);
assert_eq!(report.entries[0].key, "X");
assert_eq!(report.entries[1].key, "Y");
assert_eq!(report.entries[0].change, ChangeType::Removed);
assert_eq!(report.entries[1].change, ChangeType::Removed);
}
#[test]
fn diff_all_modified() {
let vault = secrets(&[("A", "new")]);
let artifact = secrets(&[("A", "old")]);
let report = compute_diff("development", vault, artifact);
assert_eq!(report.entries.len(), 1, "expected 1 Modified entry");
assert!(report.has_differences());
assert_eq!(report.modified, 1);
assert_eq!(report.entries[0].change, ChangeType::Modified);
assert_eq!(
report.entries[0].old_value.as_ref().map(|v| v.as_str()),
Some("old")
);
assert_eq!(
report.entries[0].new_value.as_ref().map(|v| v.as_str()),
Some("new")
);
}
#[test]
fn diff_mixed_changes() {
let vault = secrets(&[("A", "same"), ("B", "new_val"), ("D", "added")]);
let artifact = secrets(&[("A", "same"), ("B", "old_val"), ("C", "removed")]);
let report = compute_diff("development", vault, artifact);
assert_eq!(report.entries.len(), 3, "A is identical, so 3 entries");
assert_eq!(report.added, 1);
assert_eq!(report.removed, 1);
assert_eq!(report.modified, 1);
assert_eq!(report.entries[0].key, "B");
assert_eq!(report.entries[0].change, ChangeType::Modified);
assert_eq!(report.entries[1].key, "C");
assert_eq!(report.entries[1].change, ChangeType::Removed);
assert_eq!(report.entries[2].key, "D");
assert_eq!(report.entries[2].change, ChangeType::Added);
}
#[test]
fn diff_no_changes() {
let vault = secrets(&[("A", "v")]);
let artifact = secrets(&[("A", "v")]);
let report = compute_diff("development", vault, artifact);
assert!(report.entries.is_empty(), "identical values => no entries");
assert!(!report.has_differences());
assert_eq!(report.total(), 0);
}
#[test]
fn diff_empty_both() {
let report = compute_diff("development", BTreeMap::new(), BTreeMap::new());
assert!(report.entries.is_empty());
assert!(!report.has_differences());
}
#[test]
fn diff_sorted_output() {
let vault = secrets(&[("Z", "z"), ("A", "a"), ("M", "m")]);
let artifact = BTreeMap::new();
let report = compute_diff("development", vault, artifact);
assert_eq!(report.entries.len(), 3);
assert_eq!(report.entries[0].key, "A");
assert_eq!(report.entries[1].key, "M");
assert_eq!(report.entries[2].key, "Z");
}
#[test]
fn diff_values_retained_for_modified() {
let vault = secrets(&[("K", "new_secret")]);
let artifact = secrets(&[("K", "old_secret")]);
let report = compute_diff("development", vault, artifact);
assert_eq!(report.entries.len(), 1);
let entry = &report.entries[0];
assert_eq!(entry.change, ChangeType::Modified);
assert_eq!(
entry.old_value.as_ref().map(|v| v.as_str()),
Some("old_secret")
);
assert_eq!(
entry.new_value.as_ref().map(|v| v.as_str()),
Some("new_secret")
);
}
}