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 {}