use std::{ collections::{BTreeMap, BTreeSet}, net::IpAddr, }; /// Member name -> the IPs its A records point at (empty if it has none). pub type Members = BTreeMap>; /// `(member, old IPs, new IPs)` for every member whose IPs differ between two rounds. pub fn diff(prev: &Members, cur: &Members) -> Vec<(String, Vec, Vec)> { let empty = BTreeSet::new(); let names: BTreeSet<&String> = prev.keys().chain(cur.keys()).collect(); names .into_iter() .filter_map(|name| { let old = prev.get(name).unwrap_or(&empty); let new = cur.get(name).unwrap_or(&empty); (old != new).then(|| (name.clone(), old.iter().copied().collect(), new.iter().copied().collect())) }) .collect() } /// IP -> member names, so an IP that two names point at is probed and recorded once. pub fn by_ip(members: &Members) -> BTreeMap> { let mut out: BTreeMap> = BTreeMap::new(); for (name, ips) in members { for ip in ips { out.entry(*ip).or_default().push(name.clone()); } } out } #[cfg(test)] mod tests { use super::*; use std::net::Ipv4Addr; fn ip(last: u8) -> IpAddr { IpAddr::V4(Ipv4Addr::new(192, 0, 2, last)) } fn members(entries: &[(&str, &[u8])]) -> Members { entries .iter() .map(|(name, ips)| (name.to_string(), ips.iter().map(|n| ip(*n)).collect())) .collect() } #[test] fn unchanged_members_produce_no_diff() { let m = members(&[("a", &[1]), ("b", &[2])]); assert!(diff(&m, &m).is_empty()); } #[test] fn reports_changed_appeared_and_vanished_ips() { let prev = members(&[("a", &[1]), ("b", &[]), ("c", &[3])]); let cur = members(&[("a", &[2]), ("b", &[4]), ("c", &[])]); assert_eq!( diff(&prev, &cur), vec![ ("a".to_string(), vec![ip(1)], vec![ip(2)]), ("b".to_string(), vec![], vec![ip(4)]), ("c".to_string(), vec![ip(3)], vec![]), ] ); } #[test] fn by_ip_merges_names_sharing_an_ip() { let m = members(&[("a", &[1]), ("b", &[1]), ("c", &[2])]); let inv = by_ip(&m); assert_eq!(inv[&ip(1)], vec!["a".to_string(), "b".to_string()]); assert_eq!(inv[&ip(2)], vec!["c".to_string()]); } }