1use crate::error::DbError;
10use crate::json_crdt::{JsonCrdt, JsonCrdtDelta, JsonPath, JsonValue};
11use crate::rga_text::{RGAText, RGATextDelta};
12use crate::rich_text::{RichText, RichTextDelta};
13use mdcs_core::lattice::Lattice;
14use serde::{Deserialize, Serialize};
15use std::collections::{BTreeMap, HashMap};
16use ulid::Ulid;
17
18#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
20pub struct DocumentId(pub String);
21
22impl DocumentId {
23 pub fn new() -> Self {
24 Self(Ulid::new().to_string())
25 }
26
27 pub fn from_string(s: impl Into<String>) -> Self {
28 Self(s.into())
29 }
30}
31
32impl Default for DocumentId {
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38impl std::fmt::Display for DocumentId {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 write!(f, "{}", self.0)
41 }
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
46pub enum DocumentType {
47 Text,
49 RichText,
51 Json,
53}
54
55#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
57pub enum CrdtValue {
58 Text(RGAText),
60 RichText(RichText),
62 Json(JsonCrdt),
64}
65
66impl CrdtValue {
67 pub fn document_type(&self) -> DocumentType {
68 match self {
69 CrdtValue::Text(_) => DocumentType::Text,
70 CrdtValue::RichText(_) => DocumentType::RichText,
71 CrdtValue::Json(_) => DocumentType::Json,
72 }
73 }
74
75 pub fn as_text(&self) -> Option<&RGAText> {
76 match self {
77 CrdtValue::Text(t) => Some(t),
78 _ => None,
79 }
80 }
81
82 pub fn as_text_mut(&mut self) -> Option<&mut RGAText> {
83 match self {
84 CrdtValue::Text(t) => Some(t),
85 _ => None,
86 }
87 }
88
89 pub fn as_rich_text(&self) -> Option<&RichText> {
90 match self {
91 CrdtValue::RichText(rt) => Some(rt),
92 _ => None,
93 }
94 }
95
96 pub fn as_rich_text_mut(&mut self) -> Option<&mut RichText> {
97 match self {
98 CrdtValue::RichText(rt) => Some(rt),
99 _ => None,
100 }
101 }
102
103 pub fn as_json(&self) -> Option<&JsonCrdt> {
104 match self {
105 CrdtValue::Json(j) => Some(j),
106 _ => None,
107 }
108 }
109
110 pub fn as_json_mut(&mut self) -> Option<&mut JsonCrdt> {
111 match self {
112 CrdtValue::Json(j) => Some(j),
113 _ => None,
114 }
115 }
116}
117
118impl Lattice for CrdtValue {
119 fn bottom() -> Self {
120 CrdtValue::Json(JsonCrdt::bottom())
121 }
122
123 fn join(&self, other: &Self) -> Self {
124 match (self, other) {
125 (CrdtValue::Text(a), CrdtValue::Text(b)) => CrdtValue::Text(a.join(b)),
126 (CrdtValue::RichText(a), CrdtValue::RichText(b)) => CrdtValue::RichText(a.join(b)),
127 (CrdtValue::Json(a), CrdtValue::Json(b)) => CrdtValue::Json(a.join(b)),
128 _ => self.clone(),
130 }
131 }
132}
133
134#[derive(Clone, Debug, Serialize, Deserialize)]
136pub enum DocumentDelta {
137 Text(RGATextDelta),
138 RichText(RichTextDelta),
139 Json(JsonCrdtDelta),
140}
141
142#[derive(Clone, Debug, Serialize, Deserialize)]
144pub struct Document {
145 pub id: DocumentId,
147 pub title: String,
149 pub value: CrdtValue,
151 pub created_at: u64,
153 pub modified_at: u64,
155 pub metadata: HashMap<String, String>,
157}
158
159impl Document {
160 pub fn new_text(id: DocumentId, title: impl Into<String>, replica_id: &str) -> Self {
162 let now = std::time::SystemTime::now()
163 .duration_since(std::time::UNIX_EPOCH)
164 .unwrap_or_default()
165 .as_millis() as u64;
166
167 Self {
168 id,
169 title: title.into(),
170 value: CrdtValue::Text(RGAText::new(replica_id)),
171 created_at: now,
172 modified_at: now,
173 metadata: HashMap::new(),
174 }
175 }
176
177 pub fn new_rich_text(id: DocumentId, title: impl Into<String>, replica_id: &str) -> Self {
179 let now = std::time::SystemTime::now()
180 .duration_since(std::time::UNIX_EPOCH)
181 .unwrap_or_default()
182 .as_millis() as u64;
183
184 Self {
185 id,
186 title: title.into(),
187 value: CrdtValue::RichText(RichText::new(replica_id)),
188 created_at: now,
189 modified_at: now,
190 metadata: HashMap::new(),
191 }
192 }
193
194 pub fn new_json(id: DocumentId, title: impl Into<String>, replica_id: &str) -> Self {
196 let now = std::time::SystemTime::now()
197 .duration_since(std::time::UNIX_EPOCH)
198 .unwrap_or_default()
199 .as_millis() as u64;
200
201 Self {
202 id,
203 title: title.into(),
204 value: CrdtValue::Json(JsonCrdt::new(replica_id)),
205 created_at: now,
206 modified_at: now,
207 metadata: HashMap::new(),
208 }
209 }
210
211 pub fn document_type(&self) -> DocumentType {
213 self.value.document_type()
214 }
215
216 pub fn touch(&mut self) {
218 self.modified_at = std::time::SystemTime::now()
219 .duration_since(std::time::UNIX_EPOCH)
220 .unwrap_or_default()
221 .as_millis() as u64;
222 }
223
224 pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
226 self.metadata.insert(key.into(), value.into());
227 }
228
229 pub fn get_metadata(&self, key: &str) -> Option<&String> {
231 self.metadata.get(key)
232 }
233}
234
235#[derive(Clone, Debug, Default)]
237pub struct QueryOptions {
238 pub document_type: Option<DocumentType>,
240 pub title_prefix: Option<String>,
242 pub sort_by: Option<SortField>,
244 pub sort_desc: bool,
246 pub limit: Option<usize>,
248 pub offset: Option<usize>,
250}
251
252#[derive(Clone, Debug)]
253pub enum SortField {
254 Title,
255 CreatedAt,
256 ModifiedAt,
257}
258
259#[derive(Clone, Debug)]
261pub struct DocumentStore {
262 replica_id: String,
264 documents: BTreeMap<DocumentId, Document>,
266 title_index: BTreeMap<String, DocumentId>,
268 pending_changes: Vec<StoreChange>,
270}
271
272#[derive(Clone, Debug, Serialize, Deserialize)]
274pub enum StoreChange {
275 Create {
277 id: DocumentId,
278 doc_type: DocumentType,
279 title: String,
280 },
281 Update {
283 id: DocumentId,
284 delta: DocumentDelta,
285 },
286 Delete { id: DocumentId },
288 MetadataChange {
290 id: DocumentId,
291 key: String,
292 value: Option<String>,
293 },
294}
295
296impl DocumentStore {
297 pub fn new(replica_id: impl Into<String>) -> Self {
299 Self {
300 replica_id: replica_id.into(),
301 documents: BTreeMap::new(),
302 title_index: BTreeMap::new(),
303 pending_changes: Vec::new(),
304 }
305 }
306
307 pub fn replica_id(&self) -> &str {
309 &self.replica_id
310 }
311
312 pub fn create_text(&mut self, title: impl Into<String>) -> DocumentId {
316 let id = DocumentId::new();
317 let title = title.into();
318 let doc = Document::new_text(id.clone(), &title, &self.replica_id);
319
320 self.title_index.insert(title.clone(), id.clone());
321 self.documents.insert(id.clone(), doc);
322
323 self.pending_changes.push(StoreChange::Create {
324 id: id.clone(),
325 doc_type: DocumentType::Text,
326 title,
327 });
328
329 id
330 }
331
332 pub fn create_rich_text(&mut self, title: impl Into<String>) -> DocumentId {
334 let id = DocumentId::new();
335 let title = title.into();
336 let doc = Document::new_rich_text(id.clone(), &title, &self.replica_id);
337
338 self.title_index.insert(title.clone(), id.clone());
339 self.documents.insert(id.clone(), doc);
340
341 self.pending_changes.push(StoreChange::Create {
342 id: id.clone(),
343 doc_type: DocumentType::RichText,
344 title,
345 });
346
347 id
348 }
349
350 pub fn create_json(&mut self, title: impl Into<String>) -> DocumentId {
352 let id = DocumentId::new();
353 let title = title.into();
354 let doc = Document::new_json(id.clone(), &title, &self.replica_id);
355
356 self.title_index.insert(title.clone(), id.clone());
357 self.documents.insert(id.clone(), doc);
358
359 self.pending_changes.push(StoreChange::Create {
360 id: id.clone(),
361 doc_type: DocumentType::Json,
362 title,
363 });
364
365 id
366 }
367
368 pub fn get(&self, id: &DocumentId) -> Option<&Document> {
370 self.documents.get(id)
371 }
372
373 pub fn get_mut(&mut self, id: &DocumentId) -> Option<&mut Document> {
375 self.documents.get_mut(id)
376 }
377
378 pub fn delete(&mut self, id: &DocumentId) -> Option<Document> {
380 if let Some(doc) = self.documents.remove(id) {
381 self.title_index.remove(&doc.title);
382 self.pending_changes
383 .push(StoreChange::Delete { id: id.clone() });
384 Some(doc)
385 } else {
386 None
387 }
388 }
389
390 pub fn contains(&self, id: &DocumentId) -> bool {
392 self.documents.contains_key(id)
393 }
394
395 pub fn len(&self) -> usize {
397 self.documents.len()
398 }
399
400 pub fn is_empty(&self) -> bool {
402 self.documents.is_empty()
403 }
404
405 pub fn text_insert(
409 &mut self,
410 id: &DocumentId,
411 position: usize,
412 text: &str,
413 ) -> Result<(), DbError> {
414 let doc = self
415 .documents
416 .get_mut(id)
417 .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
418
419 let doc_type = doc.value.document_type();
420 let rga_text = doc.value.as_text_mut().ok_or(DbError::TypeMismatch {
421 expected: "Text".to_string(),
422 found: format!("{:?}", doc_type),
423 })?;
424
425 rga_text.insert(position, text);
426 let delta = rga_text.take_delta();
427 doc.touch();
428
429 if let Some(delta) = delta {
430 self.pending_changes.push(StoreChange::Update {
431 id: id.clone(),
432 delta: DocumentDelta::Text(delta),
433 });
434 }
435
436 Ok(())
437 }
438
439 pub fn text_delete(
441 &mut self,
442 id: &DocumentId,
443 start: usize,
444 length: usize,
445 ) -> Result<(), DbError> {
446 let doc = self
447 .documents
448 .get_mut(id)
449 .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
450
451 let doc_type = doc.value.document_type();
452 let rga_text = doc.value.as_text_mut().ok_or(DbError::TypeMismatch {
453 expected: "Text".to_string(),
454 found: format!("{:?}", doc_type),
455 })?;
456
457 rga_text.delete(start, length);
458 let delta = rga_text.take_delta();
459 doc.touch();
460
461 if let Some(delta) = delta {
462 self.pending_changes.push(StoreChange::Update {
463 id: id.clone(),
464 delta: DocumentDelta::Text(delta),
465 });
466 }
467
468 Ok(())
469 }
470
471 pub fn text_content(&self, id: &DocumentId) -> Result<String, DbError> {
473 let doc = self
474 .documents
475 .get(id)
476 .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
477
478 let rga_text = doc.value.as_text().ok_or(DbError::TypeMismatch {
479 expected: "Text".to_string(),
480 found: format!("{:?}", doc.value.document_type()),
481 })?;
482
483 Ok(rga_text.to_string())
484 }
485
486 pub fn rich_text_insert(
490 &mut self,
491 id: &DocumentId,
492 position: usize,
493 text: &str,
494 ) -> Result<(), DbError> {
495 let doc = self
496 .documents
497 .get_mut(id)
498 .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
499
500 let doc_type = doc.value.document_type();
501 let rich_text = doc.value.as_rich_text_mut().ok_or(DbError::TypeMismatch {
502 expected: "RichText".to_string(),
503 found: format!("{:?}", doc_type),
504 })?;
505
506 rich_text.insert(position, text);
507 let delta = rich_text.take_delta();
508 doc.touch();
509
510 if let Some(delta) = delta {
511 self.pending_changes.push(StoreChange::Update {
512 id: id.clone(),
513 delta: DocumentDelta::RichText(delta),
514 });
515 }
516
517 Ok(())
518 }
519
520 pub fn rich_text_bold(
522 &mut self,
523 id: &DocumentId,
524 start: usize,
525 end: usize,
526 ) -> Result<(), DbError> {
527 let doc = self
528 .documents
529 .get_mut(id)
530 .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
531
532 let doc_type = doc.value.document_type();
533 let rich_text = doc.value.as_rich_text_mut().ok_or(DbError::TypeMismatch {
534 expected: "RichText".to_string(),
535 found: format!("{:?}", doc_type),
536 })?;
537
538 rich_text.bold(start, end);
539 let delta = rich_text.take_delta();
540 doc.touch();
541
542 if let Some(delta) = delta {
543 self.pending_changes.push(StoreChange::Update {
544 id: id.clone(),
545 delta: DocumentDelta::RichText(delta),
546 });
547 }
548
549 Ok(())
550 }
551
552 pub fn rich_text_italic(
554 &mut self,
555 id: &DocumentId,
556 start: usize,
557 end: usize,
558 ) -> Result<(), DbError> {
559 let doc = self
560 .documents
561 .get_mut(id)
562 .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
563
564 let doc_type = doc.value.document_type();
565 let rich_text = doc.value.as_rich_text_mut().ok_or(DbError::TypeMismatch {
566 expected: "RichText".to_string(),
567 found: format!("{:?}", doc_type),
568 })?;
569
570 rich_text.italic(start, end);
571 let delta = rich_text.take_delta();
572 doc.touch();
573
574 if let Some(delta) = delta {
575 self.pending_changes.push(StoreChange::Update {
576 id: id.clone(),
577 delta: DocumentDelta::RichText(delta),
578 });
579 }
580
581 Ok(())
582 }
583
584 pub fn rich_text_html(&self, id: &DocumentId) -> Result<String, DbError> {
586 let doc = self
587 .documents
588 .get(id)
589 .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
590
591 let doc_type = doc.value.document_type();
592 let rich_text = doc.value.as_rich_text().ok_or(DbError::TypeMismatch {
593 expected: "RichText".to_string(),
594 found: format!("{:?}", doc_type),
595 })?;
596
597 Ok(rich_text.to_html())
598 }
599
600 pub fn json_set(
604 &mut self,
605 id: &DocumentId,
606 path: &str,
607 value: JsonValue,
608 ) -> Result<(), DbError> {
609 let doc = self
610 .documents
611 .get_mut(id)
612 .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
613
614 let doc_type = doc.value.document_type();
615 let json = doc.value.as_json_mut().ok_or(DbError::TypeMismatch {
616 expected: "Json".to_string(),
617 found: format!("{:?}", doc_type),
618 })?;
619
620 json.set(&JsonPath::parse(path), value)?;
621 let delta = json.take_delta();
622 doc.touch();
623
624 if let Some(delta) = delta {
625 self.pending_changes.push(StoreChange::Update {
626 id: id.clone(),
627 delta: DocumentDelta::Json(delta),
628 });
629 }
630
631 Ok(())
632 }
633
634 pub fn json_get(&self, id: &DocumentId, path: &str) -> Result<Option<&JsonValue>, DbError> {
636 let doc = self
637 .documents
638 .get(id)
639 .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
640
641 let doc_type = doc.value.document_type();
642 let json = doc.value.as_json().ok_or(DbError::TypeMismatch {
643 expected: "Json".to_string(),
644 found: format!("{:?}", doc_type),
645 })?;
646
647 Ok(json.get(&JsonPath::parse(path)))
648 }
649
650 pub fn json_to_value(&self, id: &DocumentId) -> Result<serde_json::Value, DbError> {
652 let doc = self
653 .documents
654 .get(id)
655 .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
656
657 let json = doc.value.as_json().ok_or(DbError::TypeMismatch {
658 expected: "Json".to_string(),
659 found: format!("{:?}", doc.value.document_type()),
660 })?;
661
662 Ok(json.to_json())
663 }
664
665 pub fn find_by_title(&self, title: &str) -> Option<&Document> {
669 self.title_index
670 .get(title)
671 .and_then(|id| self.documents.get(id))
672 }
673
674 pub fn list(&self) -> Vec<&Document> {
676 self.documents.values().collect()
677 }
678
679 pub fn query(&self, options: &QueryOptions) -> Vec<&Document> {
681 let mut results: Vec<_> = self
682 .documents
683 .values()
684 .filter(|doc| {
685 if let Some(ref doc_type) = options.document_type {
687 if &doc.document_type() != doc_type {
688 return false;
689 }
690 }
691 if let Some(ref prefix) = options.title_prefix {
693 if !doc.title.starts_with(prefix) {
694 return false;
695 }
696 }
697 true
698 })
699 .collect();
700
701 if let Some(ref sort_by) = options.sort_by {
703 match sort_by {
704 SortField::Title => {
705 results.sort_by(|a, b| a.title.cmp(&b.title));
706 }
707 SortField::CreatedAt => {
708 results.sort_by(|a, b| a.created_at.cmp(&b.created_at));
709 }
710 SortField::ModifiedAt => {
711 results.sort_by(|a, b| a.modified_at.cmp(&b.modified_at));
712 }
713 }
714 if options.sort_desc {
715 results.reverse();
716 }
717 }
718
719 if let Some(offset) = options.offset {
721 results = results.into_iter().skip(offset).collect();
722 }
723 if let Some(limit) = options.limit {
724 results.truncate(limit);
725 }
726
727 results
728 }
729
730 pub fn scan_prefix(&self, prefix: &str) -> Vec<&Document> {
732 self.title_index
733 .range(prefix.to_string()..)
734 .take_while(|(k, _)| k.starts_with(prefix))
735 .filter_map(|(_, id)| self.documents.get(id))
736 .collect()
737 }
738
739 pub fn take_changes(&mut self) -> Vec<StoreChange> {
743 std::mem::take(&mut self.pending_changes)
744 }
745
746 pub fn apply_changes(&mut self, changes: &[StoreChange]) {
748 for change in changes {
749 match change {
750 StoreChange::Create {
751 id,
752 doc_type,
753 title,
754 } => {
755 if !self.documents.contains_key(id) {
756 let doc = match doc_type {
757 DocumentType::Text => {
758 Document::new_text(id.clone(), title, &self.replica_id)
759 }
760 DocumentType::RichText => {
761 Document::new_rich_text(id.clone(), title, &self.replica_id)
762 }
763 DocumentType::Json => {
764 Document::new_json(id.clone(), title, &self.replica_id)
765 }
766 };
767 self.title_index.insert(title.clone(), id.clone());
768 self.documents.insert(id.clone(), doc);
769 }
770 }
771 StoreChange::Update { id, delta } => {
772 if let Some(doc) = self.documents.get_mut(id) {
773 match (delta, &mut doc.value) {
774 (DocumentDelta::Text(d), CrdtValue::Text(t)) => {
775 t.apply_delta(d);
776 }
777 (DocumentDelta::RichText(d), CrdtValue::RichText(rt)) => {
778 rt.apply_delta(d);
779 }
780 (DocumentDelta::Json(d), CrdtValue::Json(j)) => {
781 j.apply_delta(d);
782 }
783 _ => {} }
785 doc.touch();
786 }
787 }
788 StoreChange::Delete { id } => {
789 if let Some(doc) = self.documents.remove(id) {
790 self.title_index.remove(&doc.title);
791 }
792 }
793 StoreChange::MetadataChange { id, key, value } => {
794 if let Some(doc) = self.documents.get_mut(id) {
795 match value {
796 Some(v) => {
797 doc.metadata.insert(key.clone(), v.clone());
798 }
799 None => {
800 doc.metadata.remove(key);
801 }
802 }
803 }
804 }
805 }
806 }
807 }
808
809 pub fn document_ids(&self) -> impl Iterator<Item = &DocumentId> + '_ {
811 self.documents.keys()
812 }
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818
819 #[test]
820 fn test_create_documents() {
821 let mut store = DocumentStore::new("r1");
822
823 let text_id = store.create_text("My Text");
824 let rich_id = store.create_rich_text("My Rich Text");
825 let json_id = store.create_json("My JSON");
826
827 assert_eq!(store.len(), 3);
828 assert!(store.contains(&text_id));
829 assert!(store.contains(&rich_id));
830 assert!(store.contains(&json_id));
831 }
832
833 #[test]
834 fn test_text_operations() {
835 let mut store = DocumentStore::new("r1");
836 let id = store.create_text("Test");
837
838 store.text_insert(&id, 0, "Hello").unwrap();
839 store.text_insert(&id, 5, " World").unwrap();
840
841 let content = store.text_content(&id).unwrap();
842 assert_eq!(content, "Hello World");
843
844 store.text_delete(&id, 5, 6).unwrap();
845 let content = store.text_content(&id).unwrap();
846 assert_eq!(content, "Hello");
847 }
848
849 #[test]
850 fn test_json_operations() {
851 let mut store = DocumentStore::new("r1");
852 let id = store.create_json("Config");
853
854 store
855 .json_set(&id, "name", JsonValue::String("Test".to_string()))
856 .unwrap();
857 store.json_set(&id, "count", JsonValue::Int(42)).unwrap();
858
859 let name = store.json_get(&id, "name").unwrap();
860 assert_eq!(name.unwrap().as_str(), Some("Test"));
861
862 let json = store.json_to_value(&id).unwrap();
863 assert_eq!(json["name"], "Test");
864 assert_eq!(json["count"], 42);
865 }
866
867 #[test]
868 fn test_find_by_title() {
869 let mut store = DocumentStore::new("r1");
870
871 store.create_text("Document A");
872 store.create_text("Document B");
873 store.create_text("Other");
874
875 let doc = store.find_by_title("Document A").unwrap();
876 assert_eq!(doc.title, "Document A");
877
878 assert!(store.find_by_title("Not Found").is_none());
879 }
880
881 #[test]
882 fn test_query() {
883 let mut store = DocumentStore::new("r1");
884
885 store.create_text("Text 1");
886 store.create_text("Text 2");
887 store.create_json("Json 1");
888
889 let options = QueryOptions {
890 document_type: Some(DocumentType::Text),
891 ..Default::default()
892 };
893
894 let results = store.query(&options);
895 assert_eq!(results.len(), 2);
896 }
897
898 #[test]
899 fn test_prefix_scan() {
900 let mut store = DocumentStore::new("r1");
901
902 store.create_text("project/doc1");
903 store.create_text("project/doc2");
904 store.create_text("other/doc1");
905
906 let results = store.scan_prefix("project/");
907 assert_eq!(results.len(), 2);
908 }
909
910 #[test]
911 fn test_delete() {
912 let mut store = DocumentStore::new("r1");
913
914 let id = store.create_text("To Delete");
915 assert!(store.contains(&id));
916
917 store.delete(&id);
918 assert!(!store.contains(&id));
919 }
920
921 #[test]
922 fn test_replication() {
923 let mut store1 = DocumentStore::new("r1");
924 let mut store2 = DocumentStore::new("r2");
925
926 let id = store1.create_text("Shared Doc");
928 store1.text_insert(&id, 0, "Hello").unwrap();
929
930 let changes = store1.take_changes();
932 store2.apply_changes(&changes);
933
934 assert!(store2.contains(&id));
936 let content = store2.text_content(&id).unwrap();
937 assert_eq!(content, "Hello");
938 }
939
940 #[test]
941 fn test_metadata() {
942 let mut store = DocumentStore::new("r1");
943 let id = store.create_text("With Metadata");
944
945 let doc = store.get_mut(&id).unwrap();
946 doc.set_metadata("author", "Alice");
947 doc.set_metadata("version", "1.0");
948
949 let doc = store.get(&id).unwrap();
950 assert_eq!(doc.get_metadata("author"), Some(&"Alice".to_string()));
951 assert_eq!(doc.get_metadata("version"), Some(&"1.0".to_string()));
952 }
953}