use std::{collections::BTreeMap, fmt};
use super::lookup;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ChangeKind {
Appeared,
Disappeared,
Changed,
}
impl fmt::Display for ChangeKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
ChangeKind::Appeared => "appeared since boot",
ChangeKind::Disappeared => "disappeared since boot",
ChangeKind::Changed => "changed since boot",
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VarChange {
pub var: String,
pub kind: ChangeKind,
}
impl fmt::Display for VarChange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.var, self.kind)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Snapshot {
vars: BTreeMap<String, Option<u64>>,
}
impl Snapshot {
pub fn capture(vars: &[String], source: &mut impl FnMut(&str) -> Option<String>) -> Self {
Self {
vars: vars.iter().map(|var| (var.clone(), lookup(source, var).map(|value| fingerprint(&value)))).collect(),
}
}
pub fn diff(&self, other: &Self) -> Vec<VarChange> {
self.vars
.iter()
.filter_map(|(var, before)| {
let after = other.vars.get(var)?;
let kind = match (before, after) {
(None, Some(_)) => ChangeKind::Appeared,
(Some(_), None) => ChangeKind::Disappeared,
(Some(before), Some(after)) if before != after => ChangeKind::Changed,
_ => return None,
};
Some(VarChange { var: var.clone(), kind })
})
.collect()
}
}
#[derive(Clone, Debug)]
pub struct Watcher {
vars: Vec<String>,
at_boot: Snapshot,
}
impl Watcher {
pub fn new(vars: Vec<String>, source: &mut impl FnMut(&str) -> Option<String>) -> Self {
let at_boot = Snapshot::capture(&vars, source);
Self { vars, at_boot }
}
pub fn poll(&self, source: &mut impl FnMut(&str) -> Option<String>) -> Vec<VarChange> {
self.at_boot.diff(&Snapshot::capture(&self.vars, source))
}
pub fn at_boot(&self) -> &Snapshot {
&self.at_boot
}
}
fn fingerprint(value: &str) -> u64 {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
value.bytes().fold(OFFSET, |hash, byte| (hash ^ u64::from(byte)).wrapping_mul(PRIME))
}
#[cfg(test)]
mod tests {
use super::*;
fn source(pairs: &[(&str, &str)]) -> impl FnMut(&str) -> Option<String> {
let pairs: Vec<(String, String)> = pairs.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect();
move |var| pairs.iter().find(|(key, _)| key == var).map(|(_, value)| value.clone())
}
fn vars() -> Vec<String> {
vec!["A".to_string(), "B".to_string()]
}
#[test]
fn identical_sources_do_not_drift() {
let watcher = Watcher::new(vars(), &mut source(&[("A", "1")]));
assert!(watcher.poll(&mut source(&[("A", "1")])).is_empty());
}
#[test]
fn reports_appeared_disappeared_and_changed() {
let watcher = Watcher::new(vars(), &mut source(&[("A", "1")]));
let changes = watcher.poll(&mut source(&[("A", "2"), ("B", "new")]));
assert_eq!(
changes,
vec![
VarChange {
var: "A".to_string(),
kind: ChangeKind::Changed
},
VarChange {
var: "B".to_string(),
kind: ChangeKind::Appeared
},
]
);
let changes = watcher.poll(&mut source(&[]));
assert_eq!(
changes,
vec![VarChange {
var: "A".to_string(),
kind: ChangeKind::Disappeared
}]
);
}
#[test]
fn the_baseline_is_boot_not_the_previous_poll() {
let watcher = Watcher::new(vars(), &mut source(&[("A", "1")]));
assert_eq!(watcher.poll(&mut source(&[("A", "2")])).len(), 1);
assert_eq!(watcher.poll(&mut source(&[("A", "2")])).len(), 1);
}
#[test]
fn empty_is_unset_matches_the_parsing_contract() {
let watcher = Watcher::new(vars(), &mut source(&[("A", "")]));
assert!(watcher.poll(&mut source(&[])).is_empty(), "`A=` and no `A` are the same state");
}
#[test]
fn snapshots_never_retain_values() {
let snapshot = Snapshot::capture(&vars(), &mut source(&[("A", "hunter2")]));
assert!(!format!("{snapshot:?}").contains("hunter2"));
}
#[test]
fn undeclared_variables_are_not_drift() {
let watcher = Watcher::new(vars(), &mut source(&[("A", "1")]));
assert!(watcher.poll(&mut source(&[("A", "1"), ("UNDECLARED", "x")])).is_empty());
}
}