heaplet 0.1.0

A small, in-process, Redis-inspired in-memory store for Rust.
Documentation
//! Hash map operations (field -> value) stored under a key.
//!
//! [`HashRef`] is a Redis-inspired hash with typed values,
//! encoded/decoded via the store [`Codec`].
//!
//! Features:
//! - basic ops: `hset`, `hget`, `hdel`, `hexists`, `hlen`
//! - multi-get: `hmget`
//! - enumeration: `hkeys`, `hvals`, `hgetall`
//! - counters: `hincrby`, `hincrbyfloat`
//! - cursor scan: `hscan` (snapshot-based, deterministic paging)
//!
//! Notes:
//! - Field names are encoded canonically to keep scan ordering stable.

use serde::{Serialize, de::DeserializeOwned};

use crate::codec::{Bytes, Codec};
use crate::error::Error;
use crate::keys::matches_pattern_for_internal_use as matches_pattern;
use crate::store::Store;

/// A Redis-inspired hash map stored under a key.
///
/// Fields are stored as encoded bytes via the store's [`Codec`]. This API uses a
/// stable field encoding so that scan ordering can be deterministic.
///
/// # Semantics (MVP)
/// - Missing keys behave like an empty hash for read operations (`hget`, `hlen`, `hkeys`, ...).
/// - Mutating operations create the hash on demand.
/// - If the key exists but holds a different value type, operations return [`Error::WrongType`].
///
/// # Example
///
/// ```rust
/// use heaplet::Store;
///
/// let store = Store::new();
/// let h = store.hash("h");
///
/// // set/get
/// assert!(h.hset("a", &1_i64).unwrap());
/// let v: Option<i64> = h.hget("a").unwrap();
/// assert_eq!(v, Some(1));
///
/// // multi-get preserves field order
/// let vv: Vec<Option<i64>> = h.hmget(&["a", "missing", "a"]).unwrap();
/// assert_eq!(vv, vec![Some(1), None, Some(1)]);
///
/// // scan (snapshot + deterministic ordering)
/// let page = h.hscan::<i64>(heaplet::keys::ScanCursor(0), Some("*"), 10).unwrap();
/// assert!(page.items.len() >= 1);
/// ```
pub struct HashRef<'a, C: Codec> {
    store: &'a Store<C>,
    key: &'a str,
}

impl<'a, C: Codec> HashRef<'a, C> {
    pub(crate) fn new(store: &'a Store<C>, key: &'a str) -> Self {
        Self { store, key }
    }

    #[inline]
    fn enc<T: Serialize>(&self, v: &T) -> Result<Bytes, Error> {
        self.store.codec().encode(v)
    }

    #[inline]
    fn enc_field(&self, field: &str) -> Result<Bytes, Error> {
        // Canonical field encoding to keep scan ordering stable.
        self.store.codec().encode(field)
    }

    #[inline]
    fn dec<T: DeserializeOwned>(&self, b: &[u8]) -> Result<T, Error> {
        self.store.codec().decode(b)
    }

    /// Sets `field` to `value`.
    ///
    /// Returns `true` if the field did not exist before.
    pub fn hset<T: Serialize>(&self, field: &str, value: &T) -> Result<bool, Error> {
        let f = self.enc_field(field)?;
        let v = self.enc(value)?;
        self.store
            .with_hash_mut(self.key, |map| Ok(map.insert(f, v).is_none()))
    }

    /// Gets the value of `field`.
    ///
    /// Missing keys return `Ok(None)`.
    pub fn hget<T: DeserializeOwned>(&self, field: &str) -> Result<Option<T>, Error> {
        let f = self.enc_field(field)?;
        self.store.with_hash_read(self.key, |opt| {
            let Some(map) = opt else {
                return Ok(None);
            };
            let Some(v) = map.get(&f) else {
                return Ok(None);
            };
            Ok(Some(self.dec::<T>(v)?))
        })
    }

    /// Gets multiple fields at once.
    ///
    /// The result vector has the same length/order as `fields`.
    pub fn hmget<T: DeserializeOwned>(&self, fields: &[&str]) -> Result<Vec<Option<T>>, Error> {
        let mut out = Vec::with_capacity(fields.len());
        for f in fields {
            out.push(self.hget::<T>(f)?);
        }
        Ok(out)
    }

    /// Returns all fields and values in the hash.
    ///
    /// Missing keys return an empty vector.
    pub fn hgetall<T: DeserializeOwned>(&self) -> Result<Vec<(String, T)>, Error> {
        self.store.with_hash_read(self.key, |opt| {
            let Some(map) = opt else {
                return Ok(vec![]);
            };
            let mut out = Vec::with_capacity(map.len());
            for (k, v) in map.iter() {
                let field = self.dec::<String>(k)?;
                let val = self.dec::<T>(v)?;
                out.push((field, val));
            }
            Ok(out)
        })
    }

    /// Deletes `field`.
    ///
    /// Returns `true` if the field existed.
    pub fn hdel(&self, field: &str) -> Result<bool, Error> {
        let f = self.enc_field(field)?;
        self.store
            .with_hash_mut(self.key, |map| Ok(map.remove(&f).is_some()))
    }

    /// Returns `true` if `field` exists.
    pub fn hexists(&self, field: &str) -> Result<bool, Error> {
        let f = self.enc_field(field)?;
        self.store.with_hash_read(self.key, |opt| {
            let Some(map) = opt else {
                return Ok(false);
            };
            Ok(map.contains_key(&f))
        })
    }

    /// Returns the number of fields in the hash.
    ///
    /// Missing keys return `0`.
    pub fn hlen(&self) -> Result<usize, Error> {
        self.store
            .with_hash_read(self.key, |opt| Ok(opt.map(|m| m.len()).unwrap_or(0)))
    }

    /// Returns all field names.
    ///
    /// Missing keys return an empty vector.
    pub fn hkeys(&self) -> Result<Vec<String>, Error> {
        self.store.with_hash_read(self.key, |opt| {
            let Some(map) = opt else {
                return Ok(vec![]);
            };
            let mut out = Vec::with_capacity(map.len());
            for k in map.keys() {
                out.push(self.dec::<String>(k)?);
            }
            Ok(out)
        })
    }

    /// Returns all values.
    ///
    /// Missing keys return an empty vector.
    pub fn hvals<T: DeserializeOwned>(&self) -> Result<Vec<T>, Error> {
        self.store.with_hash_read(self.key, |opt| {
            let Some(map) = opt else {
                return Ok(vec![]);
            };
            let mut out = Vec::with_capacity(map.len());
            for v in map.values() {
                out.push(self.dec::<T>(v)?);
            }
            Ok(out)
        })
    }

    /// Increments an integer field by `delta` and returns the new value.
    pub fn hincrby(&self, field: &str, delta: i64) -> Result<i64, Error> {
        let f = self.enc_field(field)?;
        self.store.with_hash_mut(self.key, |map| {
            let cur = match map.get(&f) {
                None => 0_i64,
                Some(b) => self.dec::<i64>(b)?,
            };
            let next = cur.saturating_add(delta);
            map.insert(f, self.enc(&next)?);
            Ok(next)
        })
    }

    /// Increments a floating-point field by `delta` and returns the new value.
    pub fn hincrbyfloat(&self, field: &str, delta: f64) -> Result<f64, Error> {
        let f = self.enc_field(field)?;
        self.store.with_hash_mut(self.key, |map| {
            let cur = match map.get(&f) {
                None => 0_f64,
                Some(b) => self.dec::<f64>(b)?,
            };
            let next = cur + delta;
            map.insert(f, self.enc(&next)?);
            Ok(next)
        })
    }

    /// Cursor-based scan over fields.
    ///
    /// This implementation snapshots the hash, applies an optional pattern filter,
    /// sorts by field name, and returns a single page of items.
    ///
    /// # Pattern semantics
    /// Pattern matching uses a minimal glob-like matcher (MVP: `*` supported).
    pub fn hscan<T: DeserializeOwned>(
        &self,
        cursor: crate::keys::ScanCursor,
        pattern: Option<&str>,
        count: usize,
    ) -> Result<crate::keys::ScanPage<(String, T)>, Error> {
        let items = self.store.with_hash_read(self.key, |opt| {
            let Some(map) = opt else {
                return Ok(vec![]);
            };
            let mut out = Vec::with_capacity(map.len());
            for (k, v) in map.iter() {
                let field = self.dec::<String>(k)?;
                let val = self.dec::<T>(v)?;
                out.push((field, val));
            }
            Ok(out)
        })?;

        let mut filtered = match pattern {
            None => items,
            Some(pat) => items
                .into_iter()
                .filter(|(k, _)| matches_pattern(k, pat))
                .collect(),
        };

        if filtered.is_empty() {
            return Ok(crate::keys::ScanPage {
                cursor: crate::keys::ScanCursor(0),
                items: vec![],
            });
        }

        filtered.sort_by(|(ak, _), (bk, _)| ak.cmp(bk));

        let start = cursor.0 as usize;
        let len = filtered.len();
        if start >= len {
            return Ok(crate::keys::ScanPage {
                cursor: crate::keys::ScanCursor(0),
                items: vec![],
            });
        }

        let take = count.max(1);
        let end = (start + take).min(len);
        let next = if end >= len { 0 } else { end as u64 };

        let page_items = filtered.into_iter().skip(start).take(end - start).collect();

        Ok(crate::keys::ScanPage {
            cursor: crate::keys::ScanCursor(next),
            items: page_items,
        })
    }

    /// Removes all fields from the hash.
    ///
    /// Missing keys are treated as empty (no-op).
    pub fn hclear(&self) -> Result<(), Error> {
        self.store.with_hash_mut(self.key, |map| {
            map.clear();
            Ok(())
        })
    }
}

#[cfg(test)]
mod tests {
    use crate::Store;

    #[test]
    fn hash_basic_ops() {
        let store = Store::new();
        let h = store.hash("h_basic");

        assert_eq!(h.hlen().unwrap(), 0);
        assert!(h.hset("a", &1_i64).unwrap());
        assert!(!h.hset("a", &2_i64).unwrap()); // update
        assert_eq!(h.hlen().unwrap(), 1);

        let v: Option<i64> = h.hget("a").unwrap();
        assert_eq!(v, Some(2));

        assert!(h.hexists("a").unwrap());
        assert!(h.hdel("a").unwrap());
        assert!(!h.hexists("a").unwrap());
    }

    #[test]
    fn hash_getall_keys_vals() {
        let store = Store::new();
        let h = store.hash("h_all");
        h.hset("a", &1_i64).unwrap();
        h.hset("b", &2_i64).unwrap();

        let mut keys = h.hkeys().unwrap();
        keys.sort();
        assert_eq!(keys, vec!["a".to_string(), "b".to_string()]);

        let mut vals: Vec<i64> = h.hvals().unwrap();
        vals.sort();
        assert_eq!(vals, vec![1, 2]);

        let all: Vec<(String, i64)> = h.hgetall().unwrap();
        assert_eq!(all.len(), 2);
    }

    #[test]
    fn hash_incr() {
        let store = Store::new();
        let h = store.hash("h_incr");

        assert_eq!(h.hincrby("n", 1).unwrap(), 1);
        assert_eq!(h.hincrby("n", 5).unwrap(), 6);

        assert!((h.hincrbyfloat("f", 0.25).unwrap() - 0.25).abs() < 1e-9);
        assert!((h.hincrbyfloat("f", 1.0).unwrap() - 1.25).abs() < 1e-9);
    }

    #[test]
    fn hash_hmget_and_clear() {
        let store = Store::new();
        let h = store.hash("h_hmget");

        h.hset("a", &1_i64).unwrap();
        h.hset("b", &2_i64).unwrap();

        let v: Vec<Option<i64>> = h.hmget(&["a", "c", "b"]).unwrap();
        assert_eq!(v, vec![Some(1), None, Some(2)]);

        h.hclear().unwrap();
        assert_eq!(h.hlen().unwrap(), 0);

        let all: Vec<(String, i64)> = h.hgetall().unwrap();
        assert!(all.is_empty());
    }
}