use super::key::RefreshKey;
use serde_json::{Map, Value};
use std::cell::RefCell;
use std::collections::HashMap;
pub type PatchEntry = (Vec<String>, Map<String, Value>);
#[derive(Debug, Clone, PartialEq)]
pub enum PatchState {
Direct(Vec<PatchEntry>),
Poisoned,
}
thread_local! {
pub static TX_PATCH_MAP: RefCell<HashMap<RefreshKey, PatchState>> =
RefCell::new(HashMap::new());
}
pub fn record(key: RefreshKey, prefix: Vec<String>, fields: Map<String, Value>) -> bool {
TX_PATCH_MAP.with(|m| {
let mut map = m.borrow_mut();
match map.get_mut(&key) {
Some(PatchState::Poisoned) => false,
Some(PatchState::Direct(chain)) => {
merge_into_chain(chain, prefix, fields);
false
}
None => {
map.insert(key, PatchState::Direct(vec![(prefix, fields)]));
true
}
}
})
}
fn merge_into_chain(chain: &mut Vec<PatchEntry>, prefix: Vec<String>, fields: Map<String, Value>) {
if let Some(entry) = chain.iter_mut().find(|(p, _)| *p == prefix) {
for (k, v) in fields {
entry.1.insert(k, v); }
} else {
chain.push((prefix, fields));
}
}
pub fn poison(key: RefreshKey) {
TX_PATCH_MAP.with(|m| {
m.borrow_mut().insert(key, PatchState::Poisoned);
});
}
pub fn take_patch_snapshot() -> HashMap<RefreshKey, PatchState> {
TX_PATCH_MAP.with(|m| std::mem::take(&mut *m.borrow_mut()))
}
pub fn replace_patch_map(new_map: HashMap<RefreshKey, PatchState>) {
TX_PATCH_MAP.with(|m| *m.borrow_mut() = new_map);
}
pub fn clear_patch_map() {
TX_PATCH_MAP.with(|m| m.borrow_mut().clear());
}
pub fn merge_chain_into(
map: &mut HashMap<RefreshKey, PatchState>,
key: RefreshKey,
chain: Vec<PatchEntry>,
) {
match map.get_mut(&key) {
Some(PatchState::Poisoned) => {}
Some(PatchState::Direct(existing)) => {
for (prefix, fields) in chain {
merge_into_chain(existing, prefix, fields);
}
}
None => {
map.insert(key, PatchState::Direct(chain));
}
}
}
pub fn poison_into(map: &mut HashMap<RefreshKey, PatchState>, key: RefreshKey) {
map.insert(key, PatchState::Poisoned);
}
#[cfg(test)]
mod tests {
use super::*;
fn field(k: &str, v: &str) -> Map<String, Value> {
let mut m = Map::new();
m.insert(k.to_string(), Value::String(v.to_string()));
m
}
fn reset() {
clear_patch_map();
}
fn state_of(key: &RefreshKey) -> Option<PatchState> {
TX_PATCH_MAP.with(|m| m.borrow().get(key).cloned())
}
#[test]
fn merge_same_prefix_later_value_wins() {
reset();
let key = RefreshKey::pk("user", 1);
record(key.clone(), vec![], field("bio", "old"));
record(key.clone(), vec![], field("bio", "new"));
match state_of(&key).unwrap() {
PatchState::Direct(chain) => {
assert_eq!(chain.len(), 1, "same prefix must merge, not accumulate");
assert_eq!(chain[0].1.get("bio").unwrap(), &Value::String("new".into()));
}
PatchState::Poisoned => panic!("expected Direct"),
}
reset();
}
#[test]
fn merge_same_prefix_unions_distinct_keys() {
reset();
let key = RefreshKey::pk("user", 1);
record(key.clone(), vec![], field("bio", "b"));
record(key.clone(), vec![], field("name", "n"));
match state_of(&key).unwrap() {
PatchState::Direct(chain) => {
assert_eq!(chain.len(), 1);
assert_eq!(chain[0].1.len(), 2);
assert!(chain[0].1.contains_key("bio"));
assert!(chain[0].1.contains_key("name"));
}
PatchState::Poisoned => panic!("expected Direct"),
}
reset();
}
#[test]
fn distinct_prefixes_accumulate_entries() {
reset();
let key = RefreshKey::pk("post", 1);
record(key.clone(), vec![], field("title", "t"));
record(key.clone(), vec!["author".into()], field("bio", "b"));
match state_of(&key).unwrap() {
PatchState::Direct(chain) => {
assert_eq!(chain.len(), 2, "distinct prefixes must accumulate");
}
PatchState::Poisoned => panic!("expected Direct"),
}
reset();
}
#[test]
fn poison_is_sticky_over_later_record() {
reset();
let key = RefreshKey::pk("user", 1);
poison(key.clone());
record(key.clone(), vec![], field("bio", "new"));
assert_eq!(state_of(&key), Some(PatchState::Poisoned));
reset();
}
#[test]
fn record_then_poison_overwrites_to_poison() {
reset();
let key = RefreshKey::pk("user", 1);
record(key.clone(), vec![], field("bio", "new"));
poison(key.clone());
assert_eq!(state_of(&key), Some(PatchState::Poisoned));
reset();
}
#[test]
fn snapshot_take_clears_and_restore_round_trips() {
reset();
let key = RefreshKey::pk("user", 1);
record(key.clone(), vec![], field("bio", "b"));
let snap = take_patch_snapshot();
assert!(state_of(&key).is_none(), "take must clear the live map");
assert_eq!(snap.len(), 1);
replace_patch_map(snap);
assert!(matches!(state_of(&key), Some(PatchState::Direct(_))));
reset();
}
#[test]
fn clear_empties_the_map() {
reset();
record(RefreshKey::pk("user", 1), vec![], field("bio", "b"));
clear_patch_map();
assert!(take_patch_snapshot().is_empty());
}
}