Skip to main content

nodedb_types/
document.rs

1//! Document type: the universal data container for CRDT-backed records.
2//!
3//! Documents are the primary unit of storage, sync, and conflict resolution.
4//! Internally stored as MessagePack bytes for compact wire representation.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::value::Value;
11
12/// A CRDT-backed document. The primary data unit across all NodeDB engines.
13///
14/// Documents are schemaless: each field maps a string key to a `Value`.
15/// The `id` field is mandatory and immutable after creation.
16#[derive(
17    Debug,
18    Clone,
19    PartialEq,
20    Serialize,
21    Deserialize,
22    zerompk::ToMessagePack,
23    zerompk::FromMessagePack,
24)]
25pub struct Document {
26    /// Document identifier (unique within a collection).
27    pub id: String,
28    /// Key-value fields. Schema enforcement (if any) happens at the
29    /// collection level, not the document level.
30    pub fields: HashMap<String, Value>,
31}
32
33impl Document {
34    /// Create a new document with the given ID and no fields.
35    pub fn new(id: impl Into<String>) -> Self {
36        Self {
37            id: id.into(),
38            fields: HashMap::new(),
39        }
40    }
41
42    /// Set a field value. Returns `&mut Self` for chaining.
43    pub fn set(&mut self, key: impl Into<String>, value: Value) -> &mut Self {
44        self.fields.insert(key.into(), value);
45        self
46    }
47
48    /// Get a field value by key.
49    pub fn get(&self, key: &str) -> Option<&Value> {
50        self.fields.get(key)
51    }
52
53    /// Get a field as a string, if it exists and is a string.
54    pub fn get_str(&self, key: &str) -> Option<&str> {
55        match self.fields.get(key) {
56            Some(Value::String(s)) => Some(s),
57            _ => None,
58        }
59    }
60
61    /// Get a field as f64, if it exists and is numeric.
62    pub fn get_f64(&self, key: &str) -> Option<f64> {
63        match self.fields.get(key) {
64            Some(Value::Float(f)) => Some(*f),
65            Some(Value::Integer(i)) => Some(*i as f64),
66            _ => None,
67        }
68    }
69
70    /// Serialize the document to MessagePack bytes.
71    pub fn to_msgpack(&self) -> Result<Vec<u8>, zerompk::Error> {
72        zerompk::to_msgpack_vec(self)
73    }
74
75    /// Deserialize a document from MessagePack bytes.
76    pub fn from_msgpack(bytes: &[u8]) -> Result<Self, zerompk::Error> {
77        zerompk::from_msgpack(bytes)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn document_builder() {
87        let mut doc = Document::new("user-1");
88        doc.set("name", Value::String("Alice".into()))
89            .set("age", Value::Integer(30))
90            .set("score", Value::Float(9.5));
91
92        assert_eq!(doc.id, "user-1");
93        assert_eq!(doc.get_str("name"), Some("Alice"));
94        assert_eq!(doc.get_f64("age"), Some(30.0));
95        assert_eq!(doc.get_f64("score"), Some(9.5));
96        assert!(doc.get("missing").is_none());
97    }
98
99    #[test]
100    fn msgpack_roundtrip() {
101        let mut doc = Document::new("d1");
102        doc.set("key", Value::String("val".into()));
103
104        let bytes = doc.to_msgpack().unwrap();
105        let decoded = Document::from_msgpack(&bytes).unwrap();
106        assert_eq!(doc, decoded);
107    }
108}