hayson-engine 0.14.0

Storage engine for hayson document databases — the embeddable core behind hayson-server, hayson-cli and hayson-browser
Documentation
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());
    }
}