hayson-engine 0.22.0

Storage engine for hayson document databases — the embeddable core behind hayson-server, hayson-cli and hayson-browser
Documentation
use std::collections::{HashMap, HashSet};

use serde_json::Value;

pub fn value_key(value: &Value) -> String {
    match value {
        Value::Null => "z:".to_string(),
        Value::String(s) => format!("s:{s}"),
        Value::Number(n) => format!("n:{n}"),
        Value::Bool(b) => format!("b:{b}"),
        other => format!("j:{other}"),
    }
}

pub fn value_keys(value: Option<&Value>) -> Vec<String> {
    match value {
        None | Some(Value::Null) => Vec::new(),
        Some(Value::Array(items)) => {
            let mut seen = HashSet::new();
            let mut keys = Vec::new();
            for item in items {
                let encoded = value_key(item);
                if seen.insert(encoded.clone()) {
                    keys.push(encoded);
                }
            }
            keys
        }
        Some(other) => vec![value_key(other)],
    }
}

#[derive(Debug, Default)]
pub struct ExactIndex {
    buckets: HashMap<String, HashSet<String>>,
}

impl ExactIndex {
    pub fn new() -> Self {
        ExactIndex {
            buckets: HashMap::new(),
        }
    }

    pub fn add(&mut self, value: String, key: String) {
        self.buckets.entry(value).or_default().insert(key);
    }

    pub fn remove(&mut self, value: &str, key: &str) {
        let Some(bucket) = self.buckets.get_mut(value) else {
            return;
        };
        bucket.remove(key);
        if bucket.is_empty() {
            self.buckets.remove(value);
        }
    }

    pub fn lookup(&self, value: &str) -> Option<&HashSet<String>> {
        self.buckets.get(value)
    }

    pub fn holder_of(&self, value: &str) -> Option<&str> {
        self.buckets.get(value)?.iter().next().map(String::as_str)
    }

    #[allow(dead_code)]
    pub fn cardinality(&self) -> usize {
        self.buckets.len()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn adds_and_removes_a_key_from_its_bucket() {
        let mut index = ExactIndex::new();
        index.add("admin".into(), "user_1".into());
        index.add("admin".into(), "user_2".into());
        assert_eq!(index.cardinality(), 1);

        index.remove("admin", "user_1");
        assert_eq!(index.lookup("admin").unwrap().len(), 1);

        index.remove("admin", "user_2");
        assert!(index.lookup("admin").is_none());
        assert_eq!(index.cardinality(), 0);
    }

    #[test]
    fn does_not_confuse_a_number_with_its_string_form() {
        let number = value_key(&Value::from(31));
        let text = value_key(&Value::String("31".into()));
        assert_ne!(number, text);
    }

    #[test]
    fn dedupes_array_values_into_one_key_each() {
        let value = Value::from(vec!["backend", "backend", "node"]);
        assert_eq!(value_keys(Some(&value)).len(), 2);
    }

    #[test]
    fn treats_a_missing_field_and_an_explicit_null_the_same() {
        assert!(value_keys(None).is_empty());
        assert!(value_keys(Some(&Value::Null)).is_empty());
    }
}