Skip to main content

diamond_types_extended/
document.rs

1//! Document container and Transaction API.
2//!
3//! This module provides the main entry point for the Diamond Types Extended API:
4//! - `Document`: The unified CRDT container with a Map root
5//! - `Transaction`: Batch mutations with captured agent
6
7use std::collections::BTreeMap;
8use std::ops::Range;
9use uuid::Uuid;
10
11use crate::value::{CrdtId, MaterializedValue, PrimitiveValue, Value, checkout_to_materialized};
12use crate::{AgentId, CRDTKind, Frontier, OpLog, ROOT_CRDT_ID, LV, SerializedOpsOwned};
13use crate::refs::{MapRef, RegisterRef, SetRef, TextRef};
14use crate::muts::{MapMut, SetMut, TextMut};
15
16/// A unified CRDT document container.
17///
18/// The Document is the main entry point for Diamond Types Extended. It wraps an OpLog and provides:
19/// - A Map root for organizing data
20/// - Transaction-based mutations (solves Rust borrow issues)
21/// - Consistent API across all CRDT types
22/// - Clean serialization/replication
23///
24/// # Conflict Resolution
25///
26/// Diamond Types Extended uses deterministic conflict resolution that guarantees all peers
27/// converge to the same state:
28///
29/// - **Maps**: Each key is an LWW (Last-Writer-Wins) register. Concurrent writes
30///   are resolved by `(lamport_timestamp, agent_id)` ordering - higher timestamp
31///   wins, with agent_id as tiebreaker. Use `get_conflicted()` to see losing values.
32///
33/// - **Sets**: OR-Set (Observed-Remove) with add-wins semantics. If one peer adds
34///   a value while another removes it concurrently, the add wins.
35///
36/// - **Text**: Operations are interleaved based on causal ordering. Concurrent
37///   inserts at the same position are ordered deterministically.
38///
39/// # Transaction Isolation
40///
41/// Transactions provide a consistent view of the document. All reads within a
42/// transaction see the state as of transaction start - they do NOT see writes
43/// made earlier in the same transaction. This matches typical CRDT semantics
44/// where operations are applied to the log immediately.
45///
46/// ```
47/// use diamond_types_extended::{Document, Uuid};
48///
49/// let mut doc = Document::new();
50/// let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
51///
52/// doc.transact(alice, |tx| {
53///     tx.root().set("key", "value");
54///     // Note: get() here WILL see "value" because we read from the oplog
55///     // which has the operation applied
56/// });
57/// ```
58///
59/// # Example
60///
61/// ```
62/// use diamond_types_extended::{Document, Uuid};
63///
64/// let mut doc = Document::new();
65/// let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
66///
67/// // Mutations happen in transactions
68/// doc.transact(alice, |tx| {
69///     tx.root().set("title", "My Document");
70///     tx.root().create_text("content");
71/// });
72///
73/// // Reading is direct
74/// let title = doc.root().get("title");
75/// ```
76pub struct Document {
77    pub(crate) oplog: OpLog,
78}
79
80impl Default for Document {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86impl Document {
87    /// Create a new empty document.
88    pub fn new() -> Self {
89        Self {
90            oplog: OpLog::new(),
91        }
92    }
93
94    /// Create or retrieve an agent ID from a UUID.
95    ///
96    /// Agents represent participants in collaborative editing.
97    /// Each agent should have a unique UUID per editing session.
98    pub fn create_agent(&mut self, id: Uuid) -> AgentId {
99        self.oplog.cg.get_or_create_agent_id(id)
100    }
101
102    /// Get the current version of the document.
103    ///
104    /// This can be used for:
105    /// - Checking if documents are at the same version
106    /// - Creating patches since a version with `encode_since`
107    pub fn version(&self) -> &Frontier {
108        &self.oplog.cg.version
109    }
110
111    /// Get the document's current version as a portable `RemoteFrontier`.
112    ///
113    /// Unlike [`version()`](Self::version), which returns local-only version numbers,
114    /// the remote frontier uses `(Uuid, seq)` pairs that are safe to send to other
115    /// peers for incremental sync.
116    ///
117    /// ```
118    /// use diamond_types_extended::{Document, Uuid};
119    ///
120    /// let mut doc = Document::new();
121    /// let agent = doc.create_agent(Uuid::from_u128(0x1));
122    /// doc.transact(agent, |tx| { tx.root().set("k", "v"); });
123    ///
124    /// let remote_v = doc.remote_version();
125    /// assert_eq!(remote_v.len(), 1);
126    /// ```
127    pub fn remote_version(&self) -> crate::RemoteFrontier {
128        self.oplog.cg.agent_assignment.local_to_remote_frontier(self.version().as_ref())
129    }
130
131    /// Get operations since a remote frontier received from another peer.
132    ///
133    /// This is the safe alternative to [`ops_since()`](Self::ops_since) for
134    /// cross-document sync. It resolves the remote frontier to local versions,
135    /// gracefully handling unknown agents by returning more operations rather
136    /// than panicking.
137    ///
138    /// ```
139    /// use diamond_types_extended::{Document, Uuid, Frontier};
140    ///
141    /// let mut doc_a = Document::new();
142    /// let mut doc_b = Document::new();
143    /// let alice = doc_a.create_agent(Uuid::from_u128(0xA11CE));
144    ///
145    /// doc_a.transact(alice, |tx| { tx.root().set("k", "v"); });
146    ///
147    /// // Full sync using remote frontier
148    /// let ops = doc_a.ops_since_remote(&doc_b.remote_version()).into_owned();
149    /// doc_b.merge_ops(ops).unwrap();
150    /// assert!(doc_b.root().contains_key("k"));
151    /// ```
152    pub fn ops_since_remote(&self, remote_frontier: &crate::RemoteFrontier) -> crate::SerializedOps<'_> {
153        use crate::causalgraph::agent_assignment::remote_ids::VersionConversionError;
154
155        let local_frontier: Frontier = remote_frontier.iter()
156            .filter_map(|rv| {
157                match self.oplog.cg.agent_assignment.try_remote_to_local_version(*rv) {
158                    Ok(lv) => Some(lv),
159                    Err(VersionConversionError::SeqInFuture) => {
160                        // Remote peer is ahead of us for this agent — use our latest
161                        // version so we don't resend history we know they already have.
162                        let agent = self.oplog.cg.agent_assignment.get_agent_id(rv.0)?;
163                        let next_seq = self.oplog.cg.agent_assignment.client_data[agent as usize].get_next_seq();
164                        if next_seq > 0 {
165                            self.oplog.cg.agent_assignment.try_remote_to_local_version(
166                                crate::RemoteVersion(rv.0, (next_seq - 1) as u64)
167                            ).ok()
168                        } else {
169                            None
170                        }
171                    }
172                    Err(VersionConversionError::UnknownAgent) => None,
173                }
174            })
175            .collect();
176        self.oplog.ops_since(local_frontier.as_ref())
177    }
178
179    /// Check if the document is empty (no operations).
180    pub fn is_empty(&self) -> bool {
181        self.oplog.cg.len() == 0
182    }
183
184    // ============ Read-only access ============
185
186    /// Get a read-only reference to the root map.
187    pub fn root(&self) -> MapRef<'_> {
188        MapRef::new(&self.oplog, ROOT_CRDT_ID)
189    }
190
191    /// Get a read-only reference to a map at the given path.
192    ///
193    /// Path elements are keys in nested maps starting from root.
194    /// Returns None if path doesn't exist or isn't a Map.
195    pub fn get_map(&self, path: &[&str]) -> Option<MapRef<'_>> {
196        let (kind, crdt_id) = self.oplog.try_crdt_at_path(path)?;
197        if kind == CRDTKind::Map && crdt_id != LV::MAX {
198            Some(MapRef::new(&self.oplog, crdt_id))
199        } else {
200            None
201        }
202    }
203
204    /// Get a read-only reference to a text CRDT at the given path.
205    /// Returns None if path doesn't exist or isn't a Text.
206    pub fn get_text(&self, path: &[&str]) -> Option<TextRef<'_>> {
207        let (kind, crdt_id) = self.oplog.try_crdt_at_path(path)?;
208        if kind == CRDTKind::Text && crdt_id != LV::MAX {
209            Some(TextRef::new(&self.oplog, crdt_id))
210        } else {
211            None
212        }
213    }
214
215    /// Get a read-only reference to a set CRDT at the given path.
216    /// Returns None if path doesn't exist or isn't a Set.
217    pub fn get_set(&self, path: &[&str]) -> Option<SetRef<'_>> {
218        let (kind, crdt_id) = self.oplog.try_crdt_at_path(path)?;
219        if kind == CRDTKind::Set && crdt_id != LV::MAX {
220            Some(SetRef::new(&self.oplog, crdt_id))
221        } else {
222            None
223        }
224    }
225
226    /// Get a read-only reference to a text CRDT by its ID.
227    pub fn get_text_by_id(&self, id: CrdtId) -> Option<TextRef<'_>> {
228        if self.oplog.texts.contains_key(&id.0) {
229            Some(TextRef::new(&self.oplog, id.0))
230        } else {
231            None
232        }
233    }
234
235    /// Get a read-only reference to a set CRDT by its ID.
236    pub fn get_set_by_id(&self, id: CrdtId) -> Option<SetRef<'_>> {
237        if self.oplog.sets.contains_key(&id.0) {
238            Some(SetRef::new(&self.oplog, id.0))
239        } else {
240            None
241        }
242    }
243
244    /// Get a read-only reference to a register CRDT by its ID.
245    pub fn get_register_by_id(&self, id: CrdtId) -> Option<RegisterRef<'_>> {
246        if self.oplog.registers.contains_key(&id.0) {
247            Some(RegisterRef::new(&self.oplog, id.0))
248        } else {
249            None
250        }
251    }
252
253    // ============ Mutations via Transaction ============
254
255    /// Execute mutations in a transaction.
256    ///
257    /// The transaction captures the agent ID, so you don't need to pass it
258    /// to each mutation. This also solves Rust's borrow checker issues when
259    /// modifying nested CRDTs.
260    ///
261    /// # Example
262    ///
263    /// ```
264    /// use diamond_types_extended::{Document, Uuid};
265    ///
266    /// let mut doc = Document::new();
267    /// let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
268    ///
269    /// doc.transact(alice, |tx| {
270    ///     tx.root().set("count", 42);
271    ///     tx.root().create_map("nested");
272    ///
273    ///     // Access nested map in same transaction
274    ///     if let Some(mut nested) = tx.get_map_mut(&["nested"]) {
275    ///         nested.set("inner", "value");
276    ///     }
277    /// });
278    /// ```
279    pub fn transact<F, R>(&mut self, agent: AgentId, f: F) -> R
280    where
281        F: FnOnce(&mut Transaction) -> R,
282    {
283        let mut tx = Transaction {
284            oplog: &mut self.oplog,
285            agent,
286        };
287        f(&mut tx)
288    }
289
290    // ============ Serialization / Replication ============
291
292    /// Get operations since a version for serialization.
293    ///
294    /// Pass `&Frontier::root()` to get all operations (full sync).
295    /// Pass `peer.version()` to get only new operations (delta sync).
296    ///
297    /// Returns `SerializedOps` which borrows from this document. Use `.into()` to
298    /// convert to `SerializedOpsOwned` for sending across threads or network.
299    ///
300    /// # Example
301    ///
302    /// ```
303    /// use diamond_types_extended::{Document, Frontier, Uuid};
304    ///
305    /// let mut doc_a = Document::new();
306    /// let mut doc_b = Document::new();
307    /// let alice = doc_a.create_agent(Uuid::from_u128(0xA11CE));
308    ///
309    /// // Alice makes changes
310    /// doc_a.transact(alice, |tx| {
311    ///     tx.root().set("key", "value");
312    /// });
313    ///
314    /// // Full sync: get all operations
315    /// let all_ops = doc_a.ops_since(&Frontier::root()).into();
316    /// doc_b.merge_ops(all_ops).unwrap();
317    ///
318    /// // Now doc_b has Alice's changes
319    /// assert!(doc_b.root().contains_key("key"));
320    /// ```
321    pub fn ops_since(&self, version: &Frontier) -> crate::SerializedOps<'_> {
322        self.oplog.ops_since(version.as_ref())
323    }
324
325    /// Merge operations from another peer (owned version).
326    ///
327    /// Use this when receiving operations over a network or from another thread.
328    /// The owned version (`SerializedOpsOwned`) can be sent across thread boundaries.
329    ///
330    /// Operations are applied causally - if an operation depends on operations
331    /// you don't have yet, it will still be stored and applied correctly once
332    /// dependencies arrive.
333    pub fn merge_ops(&mut self, ops: crate::SerializedOpsOwned) -> Result<(), crate::encoding::parseerror::ParseError> {
334        self.oplog.merge_ops_owned(ops).map(|_| ())
335    }
336
337    /// Merge operations from another peer (borrowed version).
338    ///
339    /// Use this when you have a `SerializedOps` reference and don't need to
340    /// send it across threads. Slightly more efficient than `merge_ops` as it
341    /// avoids cloning strings.
342    pub fn merge_ops_borrowed(&mut self, ops: crate::SerializedOps<'_>) -> Result<(), crate::encoding::parseerror::ParseError> {
343        self.oplog.merge_ops(ops).map(|_| ())
344    }
345
346    // ============ Full state access ============
347
348    /// Get the full document state as a nested map structure.
349    ///
350    /// Returns a `BTreeMap<String, MaterializedValue>` representing the fully
351    /// resolved state of all CRDTs rooted at the document. Useful for:
352    /// - Comparing document content across peers (content convergence)
353    /// - Serialization to JSON or other formats
354    /// - Debugging and inspection
355    ///
356    /// Note: For comparing documents after sync, compare `checkout()` output
357    /// rather than `version()`. Local versions (LVs) differ across peers
358    /// after concurrent operations, but content will converge.
359    ///
360    /// # Example
361    ///
362    /// ```
363    /// use diamond_types_extended::{Document, Uuid};
364    ///
365    /// let mut doc = Document::new();
366    /// let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
367    ///
368    /// doc.transact(alice, |tx| {
369    ///     tx.root().set("key", "value");
370    ///     tx.root().create_text("content");
371    /// });
372    /// doc.transact(alice, |tx| {
373    ///     tx.get_text_mut(&["content"]).unwrap().insert(0, "Hello!");
374    /// });
375    ///
376    /// let state = doc.checkout();
377    /// // state is a BTreeMap with the full document tree
378    /// ```
379    pub fn checkout(&self) -> BTreeMap<String, MaterializedValue> {
380        checkout_to_materialized(self.oplog.checkout())
381    }
382
383    // ============ Path-based reads (FFI/WASM convenience) ============
384
385    /// Get a primitive value by key from a map at path.
386    ///
387    /// Path points to the containing map, key is the field name.
388    /// Empty path reads from the root map.
389    ///
390    /// ```
391    /// use diamond_types_extended::{Document, Uuid};
392    ///
393    /// let mut doc = Document::new();
394    /// let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
395    ///
396    /// doc.transact(alice, |tx| {
397    ///     tx.root().set("name", "Alice");
398    ///     tx.root().create_map("meta");
399    /// });
400    /// doc.transact(alice, |tx| {
401    ///     tx.get_map_mut(&["meta"]).unwrap().set("role", "admin");
402    /// });
403    ///
404    /// assert_eq!(doc.get(&[], "name").unwrap().as_str(), Some("Alice"));
405    /// assert_eq!(doc.get(&["meta"], "role").unwrap().as_str(), Some("admin"));
406    /// ```
407    pub fn get(&self, path: &[&str], key: &str) -> Option<Value> {
408        if path.is_empty() {
409            self.root().get(key)
410        } else {
411            self.get_map(path)?.get(key)
412        }
413    }
414
415    /// Get a string value by key from a map at path.
416    ///
417    /// Returns `None` if the path doesn't exist, the key doesn't exist,
418    /// or the value isn't a string.
419    pub fn get_str(&self, path: &[&str], key: &str) -> Option<String> {
420        self.get(path, key)?.as_str().map(|s| s.to_string())
421    }
422
423    /// Get an integer value by key from a map at path.
424    ///
425    /// Returns `None` if the path doesn't exist, the key doesn't exist,
426    /// or the value isn't an integer.
427    pub fn get_int(&self, path: &[&str], key: &str) -> Option<i64> {
428        self.get(path, key)?.as_int()
429    }
430
431    /// Get text content at a path (path points to the Text CRDT).
432    ///
433    /// ```
434    /// use diamond_types_extended::{Document, Uuid};
435    ///
436    /// let mut doc = Document::new();
437    /// let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
438    ///
439    /// doc.transact(alice, |tx| {
440    ///     let id = tx.root().create_text("content");
441    ///     tx.text_by_id(id).unwrap().insert(0, "Hello!");
442    /// });
443    ///
444    /// assert_eq!(doc.text_content(&["content"]).unwrap(), "Hello!");
445    /// ```
446    pub fn text_content(&self, path: &[&str]) -> Option<String> {
447        self.get_text(path).map(|t| t.content())
448    }
449
450    /// Get keys of a map at a path (owned, no borrow issues).
451    ///
452    /// Empty path returns root map keys.
453    pub fn map_keys(&self, path: &[&str]) -> Option<Vec<String>> {
454        let map = if path.is_empty() {
455            self.root()
456        } else {
457            self.get_map(path)?
458        };
459        Some(map.keys().map(|s| s.to_string()).collect())
460    }
461
462    // ============ Convenience serialization ============
463
464    /// Get owned operations since a version.
465    ///
466    /// Convenience wrapper around `ops_since()` that returns the owned type
467    /// directly, saving the `.into()` call at every use site.
468    ///
469    /// ```
470    /// use diamond_types_extended::{Document, Frontier, Uuid};
471    ///
472    /// let mut doc_a = Document::new();
473    /// let mut doc_b = Document::new();
474    /// let alice = doc_a.create_agent(Uuid::from_u128(0xA11CE));
475    ///
476    /// doc_a.transact(alice, |tx| { tx.root().set("key", "value"); });
477    ///
478    /// let ops = doc_a.ops_since_owned(&Frontier::root());
479    /// doc_b.merge_ops(ops).unwrap();
480    /// assert!(doc_b.root().contains_key("key"));
481    /// ```
482    pub fn ops_since_owned(&self, version: &Frontier) -> SerializedOpsOwned {
483        self.oplog.ops_since(version.as_ref()).into()
484    }
485
486    /// Serialize the full document state as JSON.
487    ///
488    /// Primitives are serialized naturally (strings as `"foo"`, ints as `42`,
489    /// bools as `true/false`, nil as `null`) so the output is human-friendly
490    /// regardless of the tagged enum representation used by `MaterializedValue`.
491    pub fn to_json(&self) -> String {
492        let json_val = materialized_map_to_json(self.checkout());
493        serde_json::to_string(&json_val).unwrap()
494    }
495
496    /// Serialize the full document state as pretty-printed JSON.
497    ///
498    /// See [`to_json`](Self::to_json) for details on the output format.
499    pub fn to_json_pretty(&self) -> String {
500        let json_val = materialized_map_to_json(self.checkout());
501        serde_json::to_string_pretty(&json_val).unwrap()
502    }
503
504    // ============ Closure-free mutations ============
505
506    /// Create a closure-free mutation handle for FFI/WASM consumers.
507    ///
508    /// `DocumentWriter` binds an agent to a document so mutations don't
509    /// need closures. Each method executes a single operation. For batching
510    /// multiple operations in Rust, prefer `transact()`.
511    ///
512    /// ```
513    /// use diamond_types_extended::{Document, Uuid};
514    ///
515    /// let mut doc = Document::new();
516    /// let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
517    ///
518    /// let mut w = doc.writer(alice);
519    /// w.root_set("name", "Alice");
520    /// w.root_create_text("content");
521    /// assert!(w.text_push(&["content"], "Hello!"));
522    /// assert!(!w.text_push(&["nonexistent"], "oops"));
523    ///
524    /// drop(w); // release borrow before reading
525    /// assert_eq!(doc.root().get("name").unwrap().as_str(), Some("Alice"));
526    /// ```
527    pub fn writer(&mut self, agent: AgentId) -> DocumentWriter<'_> {
528        DocumentWriter { doc: self, agent }
529    }
530
531    // ============ Internal access (for advanced use) ============
532
533    /// Get a reference to the underlying OpLog.
534    #[allow(dead_code)]
535    pub(crate) fn oplog(&self) -> &OpLog {
536        &self.oplog
537    }
538
539    /// Get a mutable reference to the underlying OpLog.
540    #[allow(dead_code)]
541    pub(crate) fn oplog_mut(&mut self) -> &mut OpLog {
542        &mut self.oplog
543    }
544}
545
546// ============ DocumentWriter — closure-free mutations ============
547
548/// Closure-free mutation handle for FFI/WASM consumers.
549///
550/// Binds an agent to a document so mutations don't need closures.
551/// Each method executes a single operation via `transact()` internally.
552/// For batching multiple operations in Rust, prefer `Document::transact()`.
553///
554/// Path-based methods return `bool` (true = success) or `Option<CrdtId>`
555/// so FFI consumers can detect invalid paths without panicking.
556///
557/// # Example
558///
559/// ```
560/// use diamond_types_extended::{Document, Uuid};
561///
562/// let mut doc = Document::new();
563/// let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
564///
565/// {
566///     let mut w = doc.writer(alice);
567///     w.root_set("title", "My Document");
568///     w.root_set("count", 42);
569///     let text_id = w.root_create_text("body");
570///     assert!(w.text_push(&["body"], "Hello, world!"));
571///     assert!(!w.text_push(&["nonexistent"], "oops"));
572/// }
573///
574/// assert_eq!(doc.root().get("title").unwrap().as_str(), Some("My Document"));
575/// assert_eq!(doc.root().get_text("body").unwrap().content(), "Hello, world!");
576/// ```
577pub struct DocumentWriter<'a> {
578    doc: &'a mut Document,
579    agent: AgentId,
580}
581
582impl<'a> DocumentWriter<'a> {
583    /// Get the agent ID bound to this writer.
584    pub fn agent(&self) -> AgentId {
585        self.agent
586    }
587
588    // ============ Map primitives (path-based) ============
589
590    /// Set a key to a primitive value in a map at path.
591    ///
592    /// Path points to the containing map. Empty path = root (always succeeds).
593    /// Returns `false` if the path doesn't resolve to a map.
594    pub fn set(&mut self, path: &[&str], key: &str, value: impl Into<PrimitiveValue>) -> bool {
595        let value = value.into();
596        self.doc.transact(self.agent, |tx| {
597            if path.is_empty() {
598                tx.root().set(key, value);
599                true
600            } else if let Some(mut map) = tx.get_map_mut(path) {
601                map.set(key, value);
602                true
603            } else {
604                false
605            }
606        })
607    }
608
609    /// Set a key to nil in a map at path.
610    ///
611    /// Returns `false` if the path doesn't resolve to a map.
612    pub fn set_nil(&mut self, path: &[&str], key: &str) -> bool {
613        self.doc.transact(self.agent, |tx| {
614            if path.is_empty() {
615                tx.root().set_nil(key);
616                true
617            } else if let Some(mut map) = tx.get_map_mut(path) {
618                map.set_nil(key);
619                true
620            } else {
621                false
622            }
623        })
624    }
625
626    // ============ CRDT creation (path-based) ============
627
628    /// Create a nested Map at key in a map at path.
629    ///
630    /// Returns `None` if the path doesn't resolve to a map.
631    pub fn create_map(&mut self, path: &[&str], key: &str) -> Option<CrdtId> {
632        self.doc.transact(self.agent, |tx| {
633            if path.is_empty() {
634                Some(tx.root().create_map(key))
635            } else {
636                tx.get_map_mut(path).map(|mut map| map.create_map(key))
637            }
638        })
639    }
640
641    /// Create a nested Text CRDT at key in a map at path.
642    ///
643    /// Returns `None` if the path doesn't resolve to a map.
644    pub fn create_text(&mut self, path: &[&str], key: &str) -> Option<CrdtId> {
645        self.doc.transact(self.agent, |tx| {
646            if path.is_empty() {
647                Some(tx.root().create_text(key))
648            } else {
649                tx.get_map_mut(path).map(|mut map| map.create_text(key))
650            }
651        })
652    }
653
654    /// Create a nested Set CRDT at key in a map at path.
655    ///
656    /// Returns `None` if the path doesn't resolve to a map.
657    pub fn create_set(&mut self, path: &[&str], key: &str) -> Option<CrdtId> {
658        self.doc.transact(self.agent, |tx| {
659            if path.is_empty() {
660                Some(tx.root().create_set(key))
661            } else {
662                tx.get_map_mut(path).map(|mut map| map.create_set(key))
663            }
664        })
665    }
666
667    /// Create a nested Register CRDT at key in a map at path.
668    ///
669    /// Returns `None` if the path doesn't resolve to a map.
670    pub fn create_register(&mut self, path: &[&str], key: &str) -> Option<CrdtId> {
671        self.doc.transact(self.agent, |tx| {
672            if path.is_empty() {
673                Some(tx.root().create_register(key))
674            } else {
675                tx.get_map_mut(path).map(|mut map| map.create_register(key))
676            }
677        })
678    }
679
680    // ============ Root-level shortcuts ============
681
682    /// Set a key to a primitive value in the root map.
683    pub fn root_set(&mut self, key: &str, value: impl Into<PrimitiveValue>) {
684        let value = value.into();
685        self.doc.transact(self.agent, |tx| {
686            tx.root().set(key, value);
687        });
688    }
689
690    /// Create a nested Map in the root map.
691    pub fn root_create_map(&mut self, key: &str) -> CrdtId {
692        self.doc.transact(self.agent, |tx| tx.root().create_map(key))
693    }
694
695    /// Create a nested Text CRDT in the root map.
696    pub fn root_create_text(&mut self, key: &str) -> CrdtId {
697        self.doc.transact(self.agent, |tx| tx.root().create_text(key))
698    }
699
700    /// Create a nested Set CRDT in the root map.
701    pub fn root_create_set(&mut self, key: &str) -> CrdtId {
702        self.doc.transact(self.agent, |tx| tx.root().create_set(key))
703    }
704
705    // ============ Text operations (path-based) ============
706
707    /// Insert text at a position in a Text CRDT at path.
708    ///
709    /// Returns `false` if the path doesn't resolve to a Text CRDT.
710    pub fn text_insert(&mut self, path: &[&str], pos: usize, content: &str) -> bool {
711        self.doc.transact(self.agent, |tx| {
712            if let Some(mut text) = tx.get_text_mut(path) {
713                text.insert(pos, content);
714                true
715            } else {
716                false
717            }
718        })
719    }
720
721    /// Delete a range of text in a Text CRDT at path.
722    ///
723    /// Returns `false` if the path doesn't resolve to a Text CRDT.
724    pub fn text_delete(&mut self, path: &[&str], range: Range<usize>) -> bool {
725        self.doc.transact(self.agent, |tx| {
726            if let Some(mut text) = tx.get_text_mut(path) {
727                text.delete(range);
728                true
729            } else {
730                false
731            }
732        })
733    }
734
735    /// Append text to the end of a Text CRDT at path.
736    ///
737    /// Returns `false` if the path doesn't resolve to a Text CRDT.
738    pub fn text_push(&mut self, path: &[&str], content: &str) -> bool {
739        self.doc.transact(self.agent, |tx| {
740            if let Some(mut text) = tx.get_text_mut(path) {
741                text.push(content);
742                true
743            } else {
744                false
745            }
746        })
747    }
748
749    // ============ Set operations (path-based) ============
750
751    /// Add a value to a Set CRDT at path.
752    ///
753    /// Returns `false` if the path doesn't resolve to a Set CRDT.
754    pub fn set_add(&mut self, path: &[&str], value: impl Into<PrimitiveValue>) -> bool {
755        let value = value.into();
756        self.doc.transact(self.agent, |tx| {
757            if let Some(mut set) = tx.get_set_mut(path) {
758                set.add(value);
759                true
760            } else {
761                false
762            }
763        })
764    }
765
766    /// Remove a value from a Set CRDT at path.
767    ///
768    /// Returns `false` if the path doesn't resolve to a Set CRDT.
769    pub fn set_remove(&mut self, path: &[&str], value: impl Into<PrimitiveValue>) -> bool {
770        let value = value.into();
771        self.doc.transact(self.agent, |tx| {
772            if let Some(mut set) = tx.get_set_mut(path) {
773                set.remove(value);
774                true
775            } else {
776                false
777            }
778        })
779    }
780}
781
782/// A transaction for batched mutations.
783///
784/// Transactions capture the agent ID so you don't need to pass it to each
785/// mutation. They also provide a clean solution to Rust's borrow checker
786/// issues when working with nested CRDTs.
787///
788/// Transactions are created by `Document::transact()` and cannot be created
789/// directly.
790pub struct Transaction<'a> {
791    pub(crate) oplog: &'a mut OpLog,
792    pub(crate) agent: AgentId,
793}
794
795impl<'a> Transaction<'a> {
796    /// Get the agent ID for this transaction.
797    pub fn agent(&self) -> AgentId {
798        self.agent
799    }
800
801    // ============ Root access ============
802
803    /// Get a mutable reference to the root map.
804    pub fn root(&mut self) -> MapMut<'_> {
805        MapMut::new(self.oplog, self.agent, ROOT_CRDT_ID)
806    }
807
808    // ============ Navigate by path ============
809
810    /// Get a mutable reference to a map at the given path.
811    /// Returns None if path doesn't exist.
812    pub fn get_map_mut(&mut self, path: &[&str]) -> Option<MapMut<'_>> {
813        let (kind, crdt_id) = self.oplog.try_crdt_at_path(path)?;
814        if kind != CRDTKind::Map || crdt_id == LV::MAX {
815            return None;
816        }
817        Some(MapMut::new(self.oplog, self.agent, crdt_id))
818    }
819
820    /// Get a mutable reference to a text CRDT at the given path.
821    /// Returns None if path doesn't exist or isn't a Text.
822    pub fn get_text_mut(&mut self, path: &[&str]) -> Option<TextMut<'_>> {
823        let (kind, crdt_id) = self.oplog.try_crdt_at_path(path)?;
824        if kind != CRDTKind::Text || crdt_id == LV::MAX {
825            return None;
826        }
827        Some(TextMut::new(self.oplog, self.agent, crdt_id))
828    }
829
830    /// Get a mutable reference to a set CRDT at the given path.
831    /// Returns None if path doesn't exist or isn't a Set.
832    pub fn get_set_mut(&mut self, path: &[&str]) -> Option<SetMut<'_>> {
833        let (kind, crdt_id) = self.oplog.try_crdt_at_path(path)?;
834        if kind != CRDTKind::Set || crdt_id == LV::MAX {
835            return None;
836        }
837        Some(SetMut::new(self.oplog, self.agent, crdt_id))
838    }
839
840    // ============ Navigate by ID ============
841
842    /// Get a mutable reference to a map by its CRDT ID.
843    pub fn map_by_id(&mut self, id: CrdtId) -> MapMut<'_> {
844        MapMut::new(self.oplog, self.agent, id.0)
845    }
846
847    /// Get a mutable reference to a text CRDT by its ID.
848    pub fn text_by_id(&mut self, id: CrdtId) -> Option<TextMut<'_>> {
849        if self.oplog.texts.contains_key(&id.0) {
850            Some(TextMut::new(self.oplog, self.agent, id.0))
851        } else {
852            None
853        }
854    }
855
856    /// Get a mutable reference to a set CRDT by its ID.
857    pub fn set_by_id(&mut self, id: CrdtId) -> Option<SetMut<'_>> {
858        if self.oplog.sets.contains_key(&id.0) {
859            Some(SetMut::new(self.oplog, self.agent, id.0))
860        } else {
861            None
862        }
863    }
864}
865
866/// Convert a `MaterializedValue` into a natural `serde_json::Value`.
867fn materialized_to_json(mv: MaterializedValue) -> serde_json::Value {
868    match mv {
869        MaterializedValue::Nil => serde_json::Value::Null,
870        MaterializedValue::Bool(b) => serde_json::Value::Bool(b),
871        MaterializedValue::Int(n) => serde_json::Value::Number(n.into()),
872        MaterializedValue::Float(n) => serde_json::json!(n.into_inner()),
873        MaterializedValue::Str(s) | MaterializedValue::Text(s) => serde_json::Value::String(s),
874        MaterializedValue::Map(m) => materialized_map_to_json(m),
875        MaterializedValue::Set(items) => serde_json::Value::Array(
876            items.into_iter().map(|pv| match pv {
877                PrimitiveValue::Nil => serde_json::Value::Null,
878                PrimitiveValue::Bool(b) => serde_json::Value::Bool(b),
879                PrimitiveValue::Int(n) => serde_json::Value::Number(n.into()),
880                PrimitiveValue::Float(n) => serde_json::json!(n.into_inner()),
881                PrimitiveValue::Str(s) => serde_json::Value::String(s),
882            }).collect(),
883        ),
884    }
885}
886
887/// Convert a checkout map into a `serde_json::Value::Object`.
888fn materialized_map_to_json(map: BTreeMap<String, MaterializedValue>) -> serde_json::Value {
889    serde_json::Value::Object(
890        map.into_iter()
891            .map(|(k, v)| (k, materialized_to_json(v)))
892            .collect(),
893    )
894}
895
896#[cfg(test)]
897mod tests {
898    use super::*;
899    use uuid::Uuid;
900
901    #[test]
902    fn test_document_new() {
903        let doc = Document::new();
904        assert!(doc.is_empty());
905    }
906
907    #[test]
908    fn test_document_agent() {
909        let mut doc = Document::new();
910        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
911        let alice2 = doc.create_agent(Uuid::from_u128(0xA11CE));
912        assert_eq!(alice, alice2);
913    }
914
915    #[test]
916    fn test_document_transact() {
917        let mut doc = Document::new();
918        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
919
920        doc.transact(alice, |tx| {
921            tx.root().set("key", "value");
922        });
923
924        let val = doc.root().get("key");
925        assert!(val.is_some());
926        assert_eq!(val.unwrap().as_str(), Some("value"));
927    }
928
929    #[test]
930    fn test_document_nested() {
931        let mut doc = Document::new();
932        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
933
934        doc.transact(alice, |tx| {
935            tx.root().create_map("nested");
936        });
937
938        // Get the nested map and set a value
939        doc.transact(alice, |tx| {
940            if let Some(mut nested) = tx.get_map_mut(&["nested"]) {
941                nested.set("inner", 42);
942            }
943        });
944
945        // Verify through the API
946        let nested = doc.root().get_map("nested");
947        assert!(nested.is_some());
948        let inner = nested.unwrap().get("inner");
949        assert!(inner.is_some());
950        assert_eq!(inner.unwrap().as_int(), Some(42));
951    }
952
953    // ============ Path-based reads ============
954
955    #[test]
956    fn test_get_from_root() {
957        let mut doc = Document::new();
958        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
959
960        doc.transact(alice, |tx| {
961            tx.root().set("name", "Alice");
962            tx.root().set("age", 30i64);
963        });
964
965        assert_eq!(doc.get(&[], "name").unwrap().as_str(), Some("Alice"));
966        assert_eq!(doc.get_str(&[], "name"), Some("Alice".to_string()));
967        assert_eq!(doc.get_int(&[], "age"), Some(30));
968        assert!(doc.get(&[], "missing").is_none());
969        assert!(doc.get_str(&[], "age").is_none()); // wrong type
970    }
971
972    #[test]
973    fn test_get_from_nested_path() {
974        let mut doc = Document::new();
975        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
976
977        doc.transact(alice, |tx| {
978            tx.root().create_map("meta");
979        });
980        doc.transact(alice, |tx| {
981            tx.get_map_mut(&["meta"]).unwrap().set("author", "Alice");
982        });
983
984        assert_eq!(doc.get_str(&["meta"], "author"), Some("Alice".to_string()));
985        assert!(doc.get_str(&["nonexistent"], "key").is_none());
986    }
987
988    #[test]
989    fn test_text_content() {
990        let mut doc = Document::new();
991        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
992
993        let id = doc.transact(alice, |tx| tx.root().create_text("body"));
994        doc.transact(alice, |tx| {
995            tx.text_by_id(id).unwrap().insert(0, "Hello!");
996        });
997
998        assert_eq!(doc.text_content(&["body"]), Some("Hello!".to_string()));
999        assert!(doc.text_content(&["missing"]).is_none());
1000    }
1001
1002    #[test]
1003    fn test_map_keys() {
1004        let mut doc = Document::new();
1005        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
1006
1007        doc.transact(alice, |tx| {
1008            tx.root().set("a", 1i64);
1009            tx.root().set("b", 2i64);
1010            tx.root().create_map("nested");
1011        });
1012
1013        let keys = doc.map_keys(&[]).unwrap();
1014        assert!(keys.contains(&"a".to_string()));
1015        assert!(keys.contains(&"b".to_string()));
1016        assert!(keys.contains(&"nested".to_string()));
1017        assert!(doc.map_keys(&["missing"]).is_none());
1018    }
1019
1020    // ============ ops_since_owned ============
1021
1022    #[test]
1023    fn test_ops_since_owned() {
1024        let mut doc_a = Document::new();
1025        let mut doc_b = Document::new();
1026        let alice = doc_a.create_agent(Uuid::from_u128(0xA11CE));
1027
1028        doc_a.transact(alice, |tx| { tx.root().set("key", "value"); });
1029
1030        let ops = doc_a.ops_since_owned(&Frontier::root());
1031        doc_b.merge_ops(ops).unwrap();
1032
1033        assert_eq!(doc_b.root().get("key").unwrap().as_str(), Some("value"));
1034    }
1035
1036    // ============ DocumentWriter ============
1037
1038    #[test]
1039    fn test_writer_root_set() {
1040        let mut doc = Document::new();
1041        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
1042
1043        {
1044            let mut w = doc.writer(alice);
1045            w.root_set("name", "Alice");
1046            w.root_set("count", 42i64);
1047            w.root_set("active", true);
1048        }
1049
1050        assert_eq!(doc.get_str(&[], "name"), Some("Alice".to_string()));
1051        assert_eq!(doc.get_int(&[], "count"), Some(42));
1052        assert_eq!(doc.get(&[], "active").unwrap().as_bool(), Some(true));
1053    }
1054
1055    #[test]
1056    fn test_writer_nested_map() {
1057        let mut doc = Document::new();
1058        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
1059
1060        {
1061            let mut w = doc.writer(alice);
1062            w.root_create_map("meta");
1063            assert!(w.set(&["meta"], "author", "Alice"));
1064            assert!(w.set(&["meta"], "year", 2025i64));
1065        }
1066
1067        assert_eq!(doc.get_str(&["meta"], "author"), Some("Alice".to_string()));
1068        assert_eq!(doc.get_int(&["meta"], "year"), Some(2025));
1069    }
1070
1071    #[test]
1072    fn test_writer_text() {
1073        let mut doc = Document::new();
1074        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
1075
1076        {
1077            let mut w = doc.writer(alice);
1078            w.root_create_text("body");
1079            assert!(w.text_insert(&["body"], 0, "Hello"));
1080            assert!(w.text_push(&["body"], ", world!"));
1081        }
1082
1083        assert_eq!(doc.text_content(&["body"]), Some("Hello, world!".to_string()));
1084
1085        {
1086            let mut w = doc.writer(alice);
1087            assert!(w.text_delete(&["body"], 5..13)); // ", world!"
1088        }
1089
1090        assert_eq!(doc.text_content(&["body"]), Some("Hello".to_string()));
1091    }
1092
1093    #[test]
1094    fn test_writer_set() {
1095        let mut doc = Document::new();
1096        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
1097
1098        {
1099            let mut w = doc.writer(alice);
1100            w.root_create_set("tags");
1101            assert!(w.set_add(&["tags"], "rust"));
1102            assert!(w.set_add(&["tags"], "crdt"));
1103            assert!(w.set_add(&["tags"], 42i64));
1104        }
1105
1106        let tags = doc.root().get_set("tags").unwrap();
1107        assert!(tags.contains_str("rust"));
1108        assert!(tags.contains_str("crdt"));
1109        assert!(tags.contains_int(42));
1110
1111        {
1112            let mut w = doc.writer(alice);
1113            assert!(w.set_remove(&["tags"], "crdt"));
1114        }
1115
1116        let tags = doc.root().get_set("tags").unwrap();
1117        assert!(tags.contains_str("rust"));
1118        assert!(!tags.contains_str("crdt"));
1119    }
1120
1121    #[test]
1122    fn test_writer_set_nil() {
1123        let mut doc = Document::new();
1124        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
1125
1126        {
1127            let mut w = doc.writer(alice);
1128            w.root_set("key", "value");
1129        }
1130        assert_eq!(doc.get_str(&[], "key"), Some("value".to_string()));
1131
1132        {
1133            let mut w = doc.writer(alice);
1134            assert!(w.set_nil(&[], "key"));
1135        }
1136        assert!(doc.get(&[], "key").unwrap().is_nil());
1137    }
1138
1139    #[test]
1140    fn test_writer_returns_false_on_bad_path() {
1141        let mut doc = Document::new();
1142        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
1143
1144        let mut w = doc.writer(alice);
1145
1146        // All path-based ops should return false for nonexistent paths
1147        assert!(!w.set(&["nope"], "k", "v"));
1148        assert!(!w.set_nil(&["nope"], "k"));
1149        assert!(!w.text_insert(&["nope"], 0, "x"));
1150        assert!(!w.text_delete(&["nope"], 0..1));
1151        assert!(!w.text_push(&["nope"], "x"));
1152        assert!(!w.set_add(&["nope"], "x"));
1153        assert!(!w.set_remove(&["nope"], "x"));
1154
1155        // create_* should return None
1156        assert!(w.create_map(&["nope"], "k").is_none());
1157        assert!(w.create_text(&["nope"], "k").is_none());
1158        assert!(w.create_set(&["nope"], "k").is_none());
1159        assert!(w.create_register(&["nope"], "k").is_none());
1160    }
1161
1162    #[test]
1163    fn test_writer_create_returns_valid_ids() {
1164        let mut doc = Document::new();
1165        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
1166
1167        let text_id;
1168        let set_id;
1169        {
1170            let mut w = doc.writer(alice);
1171            let map_id = w.root_create_map("m");
1172            text_id = w.root_create_text("t");
1173            set_id = w.root_create_set("s");
1174
1175            // path-based create on the nested map
1176            let nested = w.create_text(&["m"], "inner");
1177            assert!(nested.is_some());
1178
1179            // root shortcuts return CrdtId directly (always succeed)
1180            assert_ne!(map_id.as_lv(), LV::MAX);
1181        }
1182
1183        assert!(doc.get_map(&["m"]).is_some());
1184        assert!(doc.get_text_by_id(text_id).is_some());
1185        assert!(doc.get_set_by_id(set_id).is_some());
1186    }
1187
1188    #[test]
1189    fn test_writer_agent() {
1190        let mut doc = Document::new();
1191        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
1192
1193        let w = doc.writer(alice);
1194        assert_eq!(w.agent(), alice);
1195    }
1196
1197    #[test]
1198    fn test_writer_create_register() {
1199        let mut doc = Document::new();
1200        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
1201
1202        {
1203            let mut w = doc.writer(alice);
1204            w.root_create_map("container");
1205
1206            // path-based register creation
1207            let reg_id = w.create_register(&["container"], "reg");
1208            assert!(reg_id.is_some());
1209
1210            // verify it exists
1211            let id = reg_id.unwrap();
1212            assert!(doc.get_register_by_id(id).is_some());
1213        }
1214    }
1215
1216    #[test]
1217    fn test_to_json() {
1218        let mut doc = Document::new();
1219        let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
1220
1221        doc.transact(alice, |tx| {
1222            tx.root().set("name", "Alice");
1223            tx.root().set("age", 30i64);
1224        });
1225
1226        let json = doc.to_json();
1227        assert!(json.contains("Alice"));
1228        assert!(json.contains("30"));
1229
1230        let pretty = doc.to_json_pretty();
1231        assert!(pretty.contains('\n')); // pretty-printed has newlines
1232        assert!(pretty.contains("Alice"));
1233    }
1234
1235    // ============ Edge case tests (from Gemini review) ============
1236
1237    /// Verify that a path created by DocumentWriter is immediately
1238    /// resolvable in the next operation — no deferred indexing.
1239    #[test]
1240    fn test_writer_immediate_usage_of_created_path() {
1241        let mut doc = Document::new();
1242        let agent = doc.create_agent(Uuid::from_u128(0x7E57));
1243        let mut w = doc.writer(agent);
1244
1245        w.root_create_map("a");
1246
1247        // Create "b" inside "a" using path — relies on "a" being indexed already
1248        let b_id = w.create_map(&["a"], "b");
1249        assert!(b_id.is_some(), "child map should be creatable immediately");
1250
1251        // Write into "a/b"
1252        let success = w.set(&["a", "b"], "val", 123i64);
1253        assert!(success, "should write to deeply nested map immediately");
1254
1255        drop(w);
1256        assert_eq!(doc.get_int(&["a", "b"], "val"), Some(123));
1257    }
1258
1259    /// Verify that path-based ops fail gracefully on type mismatches:
1260    /// set() on a Text path, text_insert() on a Map path, etc.
1261    #[test]
1262    fn test_writer_path_type_mismatches() {
1263        let mut doc = Document::new();
1264        let agent = doc.create_agent(Uuid::from_u128(0x7E57));
1265        let mut w = doc.writer(agent);
1266
1267        w.root_create_text("my_text");
1268        w.root_create_map("my_map");
1269        w.root_create_set("my_set");
1270
1271        // Treat Text as a Map — set key inside text object
1272        assert!(!w.set(&["my_text"], "key", "val"));
1273
1274        // Treat Map as Text — insert text into map object
1275        assert!(!w.text_insert(&["my_map"], 0, "hello"));
1276
1277        // Treat Map as Set — add to map as if it were a set
1278        assert!(!w.set_add(&["my_map"], "val"));
1279
1280        // Treat Set as Text
1281        assert!(!w.text_push(&["my_set"], "hello"));
1282
1283        // Treat Text as Set
1284        assert!(!w.set_add(&["my_text"], "val"));
1285    }
1286
1287    /// Verify that overwriting a CRDT key with a primitive makes
1288    /// the old CRDT path unresolvable.
1289    #[test]
1290    fn test_writer_overwrite_crdt_with_primitive() {
1291        let mut doc = Document::new();
1292        let agent = doc.create_agent(Uuid::from_u128(0x7E57));
1293        let mut w = doc.writer(agent);
1294
1295        // Create a map at "config", write into it
1296        w.root_create_map("config");
1297        assert!(w.set(&["config"], "theme", "dark"));
1298
1299        drop(w);
1300        assert_eq!(doc.get_str(&["config"], "theme"), Some("dark".to_string()));
1301
1302        // Overwrite "config" with a primitive
1303        let mut w = doc.writer(agent);
1304        w.root_set("config", 404i64);
1305
1306        // Old map path should be dead
1307        assert!(!w.set(&["config"], "theme", "light"));
1308
1309        drop(w);
1310        assert_eq!(doc.get_int(&[], "config"), Some(404));
1311        assert!(doc.get_map(&["config"]).is_none());
1312    }
1313
1314    /// Verify that setting a parent to nil makes child paths unresolvable.
1315    #[test]
1316    fn test_writer_write_to_nil_parent_path() {
1317        let mut doc = Document::new();
1318        let agent = doc.create_agent(Uuid::from_u128(0x7E57));
1319        let mut w = doc.writer(agent);
1320
1321        // Setup: root -> a -> b
1322        w.root_create_map("a");
1323        w.create_map(&["a"], "b");
1324        assert!(w.set(&["a", "b"], "val", 1i64));
1325
1326        // Tombstone "a"
1327        assert!(w.set_nil(&[], "a"));
1328
1329        // Child path should be dead
1330        assert!(!w.set(&["a", "b"], "val", 2i64));
1331
1332        drop(w);
1333        assert!(doc.get(&[], "a").unwrap().is_nil());
1334    }
1335
1336    /// Verify to_json includes all CRDT types correctly.
1337    #[test]
1338    fn test_json_all_crdt_types() {
1339        let mut doc = Document::new();
1340        let agent = doc.create_agent(Uuid::from_u128(0x7E57));
1341
1342        {
1343            let mut w = doc.writer(agent);
1344            w.root_create_set("my_set");
1345            w.set_add(&["my_set"], "item1");
1346            w.set_add(&["my_set"], 42i64);
1347            w.root_create_text("my_text");
1348            w.text_push(&["my_text"], "Hello");
1349            w.root_create_map("my_map");
1350            w.set(&["my_map"], "nested_key", "nested_val");
1351        }
1352
1353        let json = doc.to_json();
1354        assert!(json.contains("item1"), "set string member");
1355        assert!(json.contains("42"), "set int member");
1356        assert!(json.contains("Hello"), "text content");
1357        assert!(json.contains("nested_val"), "nested map value");
1358    }
1359}