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(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct Document {
18    /// Document identifier (unique within a collection).
19    pub id: String,
20    /// Key-value fields. Schema enforcement (if any) happens at the
21    /// collection level, not the document level.
22    pub fields: HashMap<String, Value>,
23}
24
25impl Document {
26    /// Create a new document with the given ID and no fields.
27    pub fn new(id: impl Into<String>) -> Self {
28        Self {
29            id: id.into(),
30            fields: HashMap::new(),
31        }
32    }
33
34    /// Set a field value. Returns `&mut Self` for chaining.
35    pub fn set(&mut self, key: impl Into<String>, value: Value) -> &mut Self {
36        self.fields.insert(key.into(), value);
37        self
38    }
39
40    /// Get a field value by key.
41    pub fn get(&self, key: &str) -> Option<&Value> {
42        self.fields.get(key)
43    }
44
45    /// Get a field as a string, if it exists and is a string.
46    pub fn get_str(&self, key: &str) -> Option<&str> {
47        match self.fields.get(key) {
48            Some(Value::String(s)) => Some(s),
49            _ => None,
50        }
51    }
52
53    /// Get a field as f64, if it exists and is numeric.
54    pub fn get_f64(&self, key: &str) -> Option<f64> {
55        match self.fields.get(key) {
56            Some(Value::Float(f)) => Some(*f),
57            Some(Value::Integer(i)) => Some(*i as f64),
58            _ => None,
59        }
60    }
61
62    /// Serialize the document to MessagePack bytes.
63    pub fn to_msgpack(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
64        rmp_serde::to_vec_named(self)
65    }
66
67    /// Deserialize a document from MessagePack bytes.
68    pub fn from_msgpack(bytes: &[u8]) -> Result<Self, rmp_serde::decode::Error> {
69        rmp_serde::from_slice(bytes)
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn document_builder() {
79        let mut doc = Document::new("user-1");
80        doc.set("name", Value::String("Alice".into()))
81            .set("age", Value::Integer(30))
82            .set("score", Value::Float(9.5));
83
84        assert_eq!(doc.id, "user-1");
85        assert_eq!(doc.get_str("name"), Some("Alice"));
86        assert_eq!(doc.get_f64("age"), Some(30.0));
87        assert_eq!(doc.get_f64("score"), Some(9.5));
88        assert!(doc.get("missing").is_none());
89    }
90
91    #[test]
92    fn msgpack_roundtrip() {
93        let mut doc = Document::new("d1");
94        doc.set("key", Value::String("val".into()));
95
96        let bytes = doc.to_msgpack().unwrap();
97        let decoded = Document::from_msgpack(&bytes).unwrap();
98        assert_eq!(doc, decoded);
99    }
100}