use std::collections::{HashMap, HashSet};
pub const ALL_FIELDS: &str = "*";
#[derive(Default)]
pub struct PendingMap {
claimed: HashMap<String, HashMap<String, u32>>,
}
impl PendingMap {
pub fn new() -> Self {
Self::default()
}
pub fn pending(&self, key: &str) -> HashSet<String> {
self.claimed
.get(key)
.map(|fields| fields.keys().cloned().collect())
.unwrap_or_default()
}
pub fn conflicts_of(&self, key: &str, fields: &[String]) -> Vec<String> {
let Some(claimed) = self.claimed.get(key) else {
return Vec::new();
};
if claimed.is_empty() {
return Vec::new();
}
if claimed.contains_key(ALL_FIELDS) {
return claimed.keys().cloned().collect();
}
if fields.iter().any(|field| field == ALL_FIELDS) {
return claimed.keys().cloned().collect();
}
fields
.iter()
.filter(|field| claimed.contains_key(field.as_str()))
.cloned()
.collect()
}
pub fn claim(&mut self, key: &str, fields: &[String]) {
let claimed = self.claimed.entry(key.to_string()).or_default();
for field in fields {
*claimed.entry(field.clone()).or_insert(0) += 1;
}
}
pub fn release(&mut self, key: &str, fields: &[String]) {
let Some(claimed) = self.claimed.get_mut(key) else {
return;
};
for field in fields {
match claimed.get_mut(field) {
Some(count) if *count > 1 => *count -= 1,
Some(_) => {
claimed.remove(field);
}
None => {}
}
}
if claimed.is_empty() {
self.claimed.remove(key);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reports_no_conflict_for_an_unclaimed_key() {
let map = PendingMap::new();
assert!(map.conflicts_of("user_1", &["name".into()]).is_empty());
}
#[test]
fn reports_the_overlapping_fields_as_a_conflict() {
let mut map = PendingMap::new();
map.claim("user_1", &["name".into(), "age".into()]);
let conflicts = map.conflicts_of("user_1", &["age".into(), "email".into()]);
assert_eq!(conflicts, vec!["age".to_string()]);
}
#[test]
fn a_claim_on_all_fields_conflicts_with_any_field() {
let mut map = PendingMap::new();
map.claim("user_1", &[ALL_FIELDS.to_string()]);
assert_eq!(
map.conflicts_of("user_1", &["name".into()]),
vec![ALL_FIELDS.to_string()]
);
}
#[test]
fn a_claim_requesting_all_fields_conflicts_with_anything_held() {
let mut map = PendingMap::new();
map.claim("user_1", &["name".into()]);
let conflicts = map.conflicts_of("user_1", &[ALL_FIELDS.to_string()]);
assert_eq!(conflicts, vec!["name".to_string()]);
}
#[test]
fn release_clears_the_key_once_every_field_is_released() {
let mut map = PendingMap::new();
map.claim("user_1", &["name".into(), "age".into()]);
map.release("user_1", &["name".into()]);
assert_eq!(map.pending("user_1"), HashSet::from(["age".to_string()]));
map.release("user_1", &["age".into()]);
assert!(map.pending("user_1").is_empty());
}
#[test]
fn stacked_claims_on_the_same_field_need_the_same_number_of_releases() {
let mut map = PendingMap::new();
map.claim("user_1", &["name".into()]);
map.claim("user_1", &["name".into()]);
map.release("user_1", &["name".into()]);
assert_eq!(map.pending("user_1"), HashSet::from(["name".to_string()]));
map.release("user_1", &["name".into()]);
assert!(map.pending("user_1").is_empty());
}
}