Skip to main content

nodedb_types/
document.rs

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