Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/crdt/doc/
mod.rs

1//! Document CRDT
2//!
3//! This module provides the main public interface for a CRDT "Document" in Eidetica.
4//! The [`Doc`] type serves as the primary entry point for accessing and editing it.
5//! It is a json-ish nested type.
6//!
7//! # Usage
8//!
9//! ```
10//! use eidetica::crdt::{Doc, traits::CRDT};
11//!
12//! let mut doc = Doc::new();
13//! doc.set("name", "Alice");
14//! doc.set("age", 30);
15//! doc.set("user.profile.bio", "Software developer"); // Creates nested structure
16//!
17//! // Type-safe retrieval
18//! let name: Option<&str> = doc.get_as("name");
19//! let age: Option<i64> = doc.get_as("age");
20//!
21//! // Merge with another document
22//! let mut doc2 = Doc::new();
23//! doc2.set("name", "Bob");
24//! doc2.set("city", "New York");
25//!
26//! let merged = doc.merge(&doc2).unwrap();
27//! ```
28
29use std::{collections::HashMap, fmt};
30
31use crate::crdt::{
32    CRDTError,
33    traits::{CRDT, Data},
34};
35
36// Submodules
37pub mod list;
38#[cfg(test)]
39mod node_tests;
40pub mod path;
41pub mod value;
42
43// Convenience re-exports for core Doc types
44pub use list::List;
45pub use path::{Path, PathBuf, PathError};
46pub use value::Value;
47
48// Re-export the macro from crate root
49pub use crate::path;
50
51/// The main CRDT document type for Eidetica.
52///
53/// `Doc` is a hierarchical key-value store with Last-Write-Wins (LWW) merge semantics.
54/// Keys can be simple strings or dot-separated paths for nested access.
55///
56/// # Examples
57///
58/// ```
59/// # use eidetica::crdt::Doc;
60/// let mut doc = Doc::new();
61///
62/// // Simple key-value
63/// doc.set("name", "Alice");
64/// doc.set("age", 30);
65///
66/// // Nested paths (creates intermediate Doc nodes automatically)
67/// doc.set("user.profile.bio", "Developer");
68///
69/// // Type-safe retrieval
70/// assert_eq!(doc.get_as::<&str>("name"), Some("Alice"));
71/// assert_eq!(doc.get_as::<i64>("age"), Some(30));
72/// assert_eq!(doc.get_as::<&str>("user.profile.bio"), Some("Developer"));
73/// ```
74///
75/// # CRDT Merging
76///
77/// ```
78/// # use eidetica::crdt::{Doc, traits::CRDT};
79/// let mut doc1 = Doc::new();
80/// doc1.set("name", "Alice");
81///
82/// let mut doc2 = Doc::new();
83/// doc2.set("name", "Bob");
84/// doc2.set("city", "NYC");
85///
86/// let merged = doc1.merge(&doc2).unwrap();
87/// assert_eq!(merged.get_as::<&str>("name"), Some("Bob")); // Last write wins
88/// assert_eq!(merged.get_as::<&str>("city"), Some("NYC")); // Added from doc2
89/// ```
90/// Current CRDT format version for Doc.
91pub const DOC_VERSION: u8 = 0;
92
93/// Helper to check if version is default (0) for serde skip_serializing_if
94fn is_v0(v: &u8) -> bool {
95    *v == 0
96}
97
98/// Helper for serde skip_serializing_if on bool fields
99fn is_false(v: &bool) -> bool {
100    !v
101}
102
103/// Validates the Doc version during deserialization.
104fn validate_doc_version<'de, D>(deserializer: D) -> std::result::Result<u8, D::Error>
105where
106    D: serde::Deserializer<'de>,
107{
108    use serde::Deserialize;
109    let version = u8::deserialize(deserializer)?;
110    if version != DOC_VERSION {
111        return Err(serde::de::Error::custom(format!(
112            "unsupported Doc version {version}; only version {DOC_VERSION} is supported"
113        )));
114    }
115    Ok(version)
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
119pub struct Doc {
120    /// CRDT format version. v0 indicates unstable format.
121    #[serde(
122        rename = "_v",
123        default,
124        skip_serializing_if = "is_v0",
125        deserialize_with = "validate_doc_version"
126    )]
127    version: u8,
128    /// When true, the document merges as a single unit (LWW) rather than
129    /// recursively merging individual fields.
130    #[serde(rename = "_a", default, skip_serializing_if = "is_false")]
131    atomic: bool,
132    /// Child nodes indexed by string keys
133    children: HashMap<String, Value>,
134}
135
136impl Doc {
137    /// Creates a new empty document.
138    pub fn new() -> Self {
139        Self {
140            version: DOC_VERSION,
141            atomic: false,
142            children: HashMap::new(),
143        }
144    }
145
146    /// Creates a new empty atomic document.
147    ///
148    /// The atomic flag means "this data is a complete replacement — take all of
149    /// it." During merge (left ⊕ right):
150    ///
151    /// - `right.atomic` → LWW: return right (always replaces left entirely)
152    /// - `left.atomic`, `!right.atomic` → structural field merge, result stays
153    ///   atomic (the flag is **contagious**)
154    /// - Neither atomic → structural field merge, result non-atomic
155    ///
156    /// The contagious property preserves associativity: in a chain `1⊕2⊕3⊕4`
157    /// where 3 is atomic and 4 edits subfields, `3⊕4` produces an atomic
158    /// result, so `(1⊕2) ⊕ (3⊕4)` correctly overwrites everything before 3.
159    ///
160    /// Use this for config or metadata that must always be written as a
161    /// consistent whole. Types that need atomicity should convert into
162    /// `Doc::atomic()` (e.g., via `impl From<MyType> for Doc`) to declare
163    /// replacement semantics.
164    ///
165    /// # Examples
166    ///
167    /// ```
168    /// # use eidetica::crdt::{Doc, traits::CRDT};
169    /// let mut doc1 = Doc::atomic();
170    /// doc1.set("x", 1);
171    /// doc1.set("y", 2);
172    ///
173    /// let mut doc2 = Doc::atomic();
174    /// doc2.set("x", 10);
175    /// doc2.set("z", 30);
176    ///
177    /// // Atomic merge replaces entirely (LWW), no field-level merge
178    /// let merged = doc1.merge(&doc2).unwrap();
179    /// assert_eq!(merged.get_as::<i64>("x"), Some(10));
180    /// assert_eq!(merged.get_as::<i64>("z"), Some(30));
181    /// assert_eq!(merged.get_as::<i64>("y"), None); // Not carried from doc1
182    /// ```
183    pub fn atomic() -> Self {
184        Self {
185            version: DOC_VERSION,
186            atomic: true,
187            children: HashMap::new(),
188        }
189    }
190
191    /// Returns true if this document uses atomic merge semantics.
192    pub fn is_atomic(&self) -> bool {
193        self.atomic
194    }
195
196    /// Returns true if this document has no data (excluding tombstones).
197    pub fn is_empty(&self) -> bool {
198        self.children.values().all(|v| matches!(v, Value::Deleted))
199    }
200
201    /// Returns the number of direct keys (excluding tombstones).
202    pub fn len(&self) -> usize {
203        self.children
204            .values()
205            .filter(|v| !matches!(v, Value::Deleted))
206            .count()
207    }
208
209    /// Returns true if the document contains the given key.
210    pub fn contains_key(&self, key: impl AsRef<Path>) -> bool {
211        self.get(key).is_some()
212    }
213
214    /// Returns true if the exact path points to a tombstone (Value::Deleted).
215    ///
216    /// This method checks if the specific key has been deleted. Note that this
217    /// only returns true if the exact path is a tombstone - it does not check
218    /// if an ancestor was deleted (which would make the path inaccessible).
219    ///
220    /// To check if a path is inaccessible (either deleted or has a deleted ancestor),
221    /// use `get(path).is_none()` instead.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// # use eidetica::crdt::Doc;
227    /// let mut doc = Doc::new();
228    /// doc.set("user.profile.name", "Alice");
229    /// doc.remove("user.profile.name");
230    ///
231    /// assert!(doc.is_tombstone("user.profile.name"));
232    /// assert!(!doc.is_tombstone("user.profile")); // parent is not tombstoned
233    ///
234    /// // Deleting a parent makes children inaccessible but not directly tombstoned
235    /// doc.set("settings.theme.color", "blue");
236    /// doc.remove("settings.theme");
237    /// assert!(doc.is_tombstone("settings.theme")); // exact path is tombstoned
238    /// assert!(!doc.is_tombstone("settings.theme.color")); // child path is NOT a tombstone
239    /// assert!(doc.get("settings.theme.color").is_none()); // but it's still inaccessible
240    /// ```
241    pub fn is_tombstone(&self, key: impl AsRef<Path>) -> bool {
242        matches!(self.get_raw(key), Some(Value::Deleted))
243    }
244
245    /// Gets a value by key or path without filtering tombstones.
246    fn get_raw(&self, key: impl AsRef<Path>) -> Option<&Value> {
247        let path = key.as_ref();
248        let path_str: &str = path.as_ref();
249
250        // For simple keys (no dots), use direct access
251        // This handles empty keys ("") and regular simple keys ("foo")
252        if !path_str.contains('.') {
253            return self.children.get(path_str);
254        }
255
256        // For paths with dots, use components (which filters empty strings)
257        let segments: Vec<_> = path.components().collect();
258
259        if segments.is_empty() {
260            return None;
261        }
262
263        let first_segment = segments.first()?;
264        let mut current_value = self.children.get(*first_segment)?;
265
266        for segment in &segments[1..] {
267            match current_value {
268                Value::Doc(doc) => {
269                    current_value = doc.children.get(*segment)?;
270                }
271                Value::List(list) => {
272                    // Try to parse segment as list index
273                    let index: usize = segment.parse().ok()?;
274                    current_value = list.get(index)?;
275                }
276                // Can't navigate through Deleted, scalars, etc.
277                _ => return None,
278            }
279        }
280
281        Some(current_value)
282    }
283
284    /// Gets a value by key or path (immutable reference).
285    ///
286    /// Supports both simple keys and dot-separated paths for nested access.
287    /// Returns `None` if the key doesn't exist or has been deleted (tombstone).
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// # use eidetica::crdt::Doc;
293    /// let mut doc = Doc::new();
294    /// doc.set("name", "Alice");
295    /// doc.set("user.profile.age", 30);
296    ///
297    /// assert!(doc.get("name").is_some());
298    /// assert!(doc.get("user.profile.age").is_some());
299    /// assert!(doc.get("nonexistent").is_none());
300    ///
301    /// // Deleted keys return None
302    /// doc.remove("name");
303    /// assert!(doc.get("name").is_none());
304    /// ```
305    pub fn get(&self, key: impl AsRef<Path>) -> Option<&Value> {
306        match self.get_raw(key) {
307            Some(Value::Deleted) => None,
308            value => value,
309        }
310    }
311
312    /// Gets a mutable reference to a value by key or path
313    pub fn get_mut(&mut self, key: impl AsRef<Path>) -> Option<&mut Value> {
314        let path = key.as_ref();
315        let path_str: &str = path.as_ref();
316
317        // For simple keys (no dots), use direct access
318        // This handles empty keys ("") and regular simple keys ("foo")
319        if !path_str.contains('.') {
320            return match self.children.get_mut(path_str) {
321                Some(Value::Deleted) => None, // Hide tombstones
322                value => value,
323            };
324        }
325
326        // For paths with dots, use components (which filters empty strings)
327        let segments: Vec<_> = path.components().collect();
328
329        if segments.is_empty() {
330            return None;
331        }
332
333        let mut current = self;
334
335        // Navigate to the parent of the target
336        for segment in &segments[..segments.len() - 1] {
337            match current.children.get_mut(*segment) {
338                Some(Value::Doc(doc)) => {
339                    // Now Value::Doc contains a Doc, so we can navigate
340                    current = doc;
341                }
342                _ => return None, // Can't navigate further
343            }
344        }
345
346        // Get the final value
347        let final_key = segments.last()?;
348        match current.children.get_mut(*final_key) {
349            Some(Value::Deleted) => None, // Hide tombstones
350            value => value,
351        }
352    }
353
354    /// Gets a value by key with automatic type conversion using TryFrom
355    ///
356    /// Returns Some(T) if the value exists and can be converted to type T.
357    /// Returns None if the key doesn't exist or type conversion fails.
358    ///
359    /// This is the recommended method for type-safe value retrieval as it provides
360    /// a cleaner Option-based interface compared to the Result-based `get_as()` method.
361    ///
362    /// # Examples
363    ///
364    /// ```
365    /// # use eidetica::crdt::Doc;
366    /// let mut doc = Doc::new();
367    /// doc.set("name", "Alice");
368    /// doc.set("age", 30);
369    /// doc.set("active", true);
370    ///
371    /// // Returns Some when value exists and type matches
372    /// assert_eq!(doc.get_as::<&str>("name"), Some("Alice"));
373    /// assert_eq!(doc.get_as::<i64>("age"), Some(30));
374    /// assert_eq!(doc.get_as::<bool>("active"), Some(true));
375    ///
376    /// // Returns None when key doesn't exist
377    /// assert_eq!(doc.get_as::<String>("missing"), None);
378    ///
379    /// // Returns None when type doesn't match
380    /// assert_eq!(doc.get_as::<i64>("name"), None);
381    /// ```
382    pub fn get_as<'a, T>(&'a self, key: impl AsRef<Path>) -> Option<T>
383    where
384        T: TryFrom<&'a Value, Error = CRDTError>,
385    {
386        let value = self.get(key)?;
387        T::try_from(value).ok()
388    }
389
390    /// Sets a value at the given key or path, returns the old value if present.
391    ///
392    /// This method automatically creates intermediate `Doc` nodes for nested paths.
393    /// For example, `doc.set("a.b.c", value)` will create `a` and `b` as `Doc` nodes
394    /// if they don't exist.
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// # use eidetica::crdt::Doc;
400    /// let mut doc = Doc::new();
401    ///
402    /// // Simple key
403    /// doc.set("name", "Alice");
404    ///
405    /// // Nested path - creates intermediate nodes automatically
406    /// doc.set("user.profile.age", 30);
407    ///
408    /// assert_eq!(doc.get_as("name"), Some("Alice"));
409    /// assert_eq!(doc.get_as("user.profile.age"), Some(30));
410    /// ```
411    pub fn set(&mut self, key: impl AsRef<Path>, value: impl Into<Value>) -> Option<Value> {
412        let path = key.as_ref();
413        let path_str: &str = path.as_ref();
414
415        // For simple keys (no dots), use direct assignment
416        // This handles empty keys ("") and regular simple keys ("foo")
417        if !path_str.contains('.') {
418            let old = self.children.insert(path_str.to_string(), value.into());
419            return match old {
420                Some(Value::Deleted) => None, // Don't return tombstones
421                v => v,
422            };
423        }
424
425        // For paths with dots, use components (which filters empty strings)
426        let segments: Vec<_> = path.components().collect();
427
428        if segments.is_empty() {
429            return None;
430        }
431
432        // Single segment after filtering - direct assignment
433        if segments.len() == 1 {
434            let old = self.children.insert(segments[0].to_string(), value.into());
435            return match old {
436                Some(Value::Deleted) => None, // Don't return tombstones
437                v => v,
438            };
439        }
440
441        // Navigate to the parent, creating intermediate nodes as needed
442        let mut current = self;
443        for segment in &segments[..segments.len() - 1] {
444            let entry = current
445                .children
446                .entry(segment.to_string())
447                .or_insert_with(|| Value::Doc(Doc::new()));
448            match entry {
449                Value::Doc(doc) => {
450                    current = doc;
451                }
452                Value::Deleted => {
453                    // Replace tombstone with new node
454                    *entry = Value::Doc(Doc::new());
455                    match entry {
456                        Value::Doc(doc) => current = doc,
457                        _ => unreachable!(),
458                    }
459                }
460                _ => {
461                    // Replace scalar value with new node to allow navigation
462                    *entry = Value::Doc(Doc::new());
463                    match entry {
464                        Value::Doc(doc) => current = doc,
465                        _ => unreachable!(),
466                    }
467                }
468            }
469        }
470
471        // Set the final value
472        let final_key = segments.last().unwrap();
473        let old = current.children.insert(final_key.to_string(), value.into());
474
475        match old {
476            Some(Value::Deleted) => None, // Don't return tombstones
477            v => v,
478        }
479    }
480
481    /// Removes a value by key or path, returns the old value if present.
482    ///
483    /// This method implements CRDT semantics by always creating a tombstone marker.
484    /// For nested paths, intermediate Doc nodes are created if they don't exist.
485    ///
486    /// # Examples
487    ///
488    /// ```
489    /// # use eidetica::crdt::Doc;
490    /// let mut doc = Doc::new();
491    /// doc.set("user.profile.name", "Alice");
492    ///
493    /// let old = doc.remove("user.profile.name");
494    /// assert_eq!(old.and_then(|v| v.as_text().map(|s| s.to_string())), Some("Alice".to_string()));
495    /// assert!(doc.get("user.profile.name").is_none());
496    /// ```
497    pub fn remove(&mut self, key: impl AsRef<Path>) -> Option<Value> {
498        // Delegate to set with Value::Deleted
499        self.set(key, Value::Deleted)
500    }
501
502    /// Returns an iterator over all key-value pairs (excluding tombstones)
503    pub fn iter(&self) -> impl Iterator<Item = (&String, &Value)> {
504        self.children
505            .iter()
506            .filter(|(_, v)| !matches!(v, Value::Deleted))
507    }
508
509    /// Returns an iterator over all keys (excluding tombstones)
510    pub fn keys(&self) -> impl Iterator<Item = &String> {
511        self.children
512            .iter()
513            .filter(|(_, v)| !matches!(v, Value::Deleted))
514            .map(|(k, _)| k)
515    }
516
517    /// Returns an iterator over all values (excluding tombstones)
518    pub fn values(&self) -> impl Iterator<Item = &Value> {
519        self.children
520            .values()
521            .filter(|v| !matches!(v, Value::Deleted))
522    }
523
524    /// Converts this Doc to a JSON string representation.
525    ///
526    /// This produces a valid JSON object string from the document's contents,
527    /// excluding tombstones.
528    pub fn to_json_string(&self) -> String {
529        // 64-byte preallocated buffer to avoid reallocs for simple cases
530        let mut result = String::with_capacity(64);
531        result.push('{');
532        let mut first = true;
533        for (key, value) in self.iter() {
534            if !first {
535                result.push(',');
536            }
537            result.push_str(&format!("\"{}\":{}", key, value.to_json_string()));
538            first = false;
539        }
540        result.push('}');
541        result
542    }
543}
544
545impl CRDT for Doc {
546    /// Merges another document into this one using CRDT semantics.
547    ///
548    /// This implements the core CRDT merge operation at the document level,
549    /// providing deterministic conflict resolution following CRDT semantics.
550    ///
551    /// Merge combines the key-value pairs from both documents, resolving
552    /// conflicts deterministically using CRDT rules.
553    fn merge(&self, other: &Self) -> crate::Result<Self> {
554        if other.atomic {
555            return Ok(other.clone());
556        }
557        let mut result = self.clone();
558        for (key, other_value) in &other.children {
559            match result.children.get_mut(key) {
560                Some(self_value) => {
561                    self_value.merge(other_value);
562                }
563                None => {
564                    result.children.insert(key.clone(), other_value.clone());
565                }
566            }
567        }
568        Ok(result)
569    }
570}
571
572impl Default for Doc {
573    fn default() -> Self {
574        Self::new()
575    }
576}
577
578impl fmt::Display for Doc {
579    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
580        write!(f, "{{")?;
581        let mut first = true;
582        for (key, value) in self.iter() {
583            if !first {
584                write!(f, ", ")?;
585            }
586            write!(f, "{key}: {value}")?;
587            first = false;
588        }
589        write!(f, "}}")
590    }
591}
592
593impl FromIterator<(String, Value)> for Doc {
594    fn from_iter<T: IntoIterator<Item = (String, Value)>>(iter: T) -> Self {
595        let mut doc = Doc::new();
596        for (key, value) in iter {
597            doc.set(key, value);
598        }
599        doc
600    }
601}
602
603// JSON serialization methods
604impl Doc {
605    /// Set a key-value pair with automatic JSON serialization for any Serialize type.
606    ///
607    /// The value is serialized to a JSON string and stored as `Value::Text`.
608    ///
609    /// # Examples
610    ///
611    /// ```
612    /// # use eidetica::crdt::Doc;
613    /// use serde::{Serialize, Deserialize};
614    ///
615    /// #[derive(Serialize, Deserialize, PartialEq, Debug)]
616    /// struct User { name: String, age: i32 }
617    ///
618    /// let mut doc = Doc::new();
619    /// doc.set_json("user", User { name: "Alice".into(), age: 30 })?;
620    ///
621    /// let user: User = doc.get_json("user")?;
622    /// assert_eq!(user, User { name: "Alice".into(), age: 30 });
623    /// # Ok::<(), eidetica::Error>(())
624    /// ```
625    pub fn set_json<T>(&mut self, key: impl AsRef<Path>, value: T) -> crate::Result<&mut Self>
626    where
627        T: serde::Serialize,
628    {
629        let json = serde_json::to_string(&value).map_err(|e| CRDTError::SerializationFailed {
630            reason: e.to_string(),
631        })?;
632        self.set(key, Value::Text(json));
633        Ok(self)
634    }
635
636    /// Get a value by key with automatic JSON deserialization for any Deserialize type.
637    ///
638    /// The value must be a `Value::Text` containing valid JSON.
639    ///
640    /// # Errors
641    ///
642    /// Returns an error if:
643    /// - The key doesn't exist
644    /// - The value is not a `Value::Text`
645    /// - The JSON deserialization fails
646    pub fn get_json<T>(&self, key: impl AsRef<Path>) -> crate::Result<T>
647    where
648        T: for<'de> serde::Deserialize<'de>,
649    {
650        let path_str = key.as_ref().as_str().to_string();
651        let value = self.get(key).ok_or_else(|| CRDTError::ElementNotFound {
652            key: path_str.clone(),
653        })?;
654
655        match value {
656            Value::Text(json) => serde_json::from_str(json).map_err(|e| {
657                CRDTError::DeserializationFailed {
658                    reason: format!("Failed to deserialize JSON for key '{path_str}': {e}"),
659                }
660                .into()
661            }),
662            _ => Err(CRDTError::TypeMismatch {
663                expected: "Text (JSON string)".to_string(),
664                actual: value.type_name().to_string(),
665            }
666            .into()),
667        }
668    }
669
670    /// Gets or inserts a value with a default, returns a mutable reference.
671    ///
672    /// If the key doesn't exist, the default value is inserted. Returns a mutable
673    /// reference to the value (existing or newly inserted).
674    ///
675    /// # Examples
676    ///
677    /// ```
678    /// # use eidetica::crdt::Doc;
679    /// let mut doc = Doc::new();
680    ///
681    /// // Key doesn't exist - will insert default
682    /// doc.get_or_insert("counter", 0);
683    /// assert_eq!(doc.get_as::<i64>("counter"), Some(0));
684    ///
685    /// // Key exists - will keep existing value
686    /// doc.set("counter", 5);
687    /// doc.get_or_insert("counter", 100);
688    /// assert_eq!(doc.get_as::<i64>("counter"), Some(5));
689    /// ```
690    pub fn get_or_insert(
691        &mut self,
692        key: impl AsRef<Path> + Clone,
693        default: impl Into<Value>,
694    ) -> &mut Value {
695        if !self.contains_key(key.clone()) {
696            self.set(key.clone(), default);
697        }
698        self.get_mut(key).expect("Key should exist after insert")
699    }
700}
701
702// Conversion implementations
703// Node is now an alias for Doc, so no conversion implementations needed
704// The standard From<T> for T implementation in core handles this automatically
705
706// Data trait implementation
707impl Data for Doc {}