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}