1mod accessors;
7#[cfg(feature = "document-cbor-conversion")]
8pub(super) mod cbor_conversion;
9#[cfg(feature = "value-conversion")]
10mod platform_value_conversion;
11pub mod serialize;
12
13use chrono::DateTime;
14use std::collections::BTreeMap;
15use std::fmt;
16
17use platform_value::Value;
18
19use crate::document::document_methods::{
20 DocumentGetRawForContractV0, DocumentGetRawForDocumentTypeV0, DocumentHashV0Method,
21 DocumentIsEqualIgnoringTimestampsV0,
22};
23
24use crate::identity::TimestampMillis;
25use crate::prelude::Revision;
26use crate::prelude::{BlockHeight, CoreBlockHeight, Identifier};
27#[cfg(feature = "json-conversion")]
28use crate::serialization::json_safe_fields;
29
30#[cfg_attr(feature = "json-conversion", json_safe_fields)]
32#[derive(Clone, Debug, PartialEq, Default)]
33#[cfg_attr(
34 feature = "serde-conversion",
35 derive(serde::Serialize, serde::Deserialize)
36)]
37pub struct DocumentV0 {
38 #[cfg_attr(feature = "serde-conversion", serde(rename = "$id"))]
40 pub id: Identifier,
41 #[cfg_attr(feature = "serde-conversion", serde(rename = "$ownerId"))]
43 pub owner_id: Identifier,
44 #[cfg_attr(feature = "serde-conversion", serde(flatten))]
46 pub properties: BTreeMap<String, Value>,
47 #[cfg_attr(feature = "serde-conversion", serde(rename = "$revision", default))]
49 pub revision: Option<Revision>,
50 #[cfg_attr(feature = "serde-conversion", serde(rename = "$createdAt", default))]
52 pub created_at: Option<TimestampMillis>,
53 #[cfg_attr(feature = "serde-conversion", serde(rename = "$updatedAt", default))]
55 pub updated_at: Option<TimestampMillis>,
56 #[cfg_attr(
58 feature = "serde-conversion",
59 serde(rename = "$transferredAt", default)
60 )]
61 pub transferred_at: Option<TimestampMillis>,
62 #[cfg_attr(
64 feature = "serde-conversion",
65 serde(rename = "$createdAtBlockHeight", default)
66 )]
67 pub created_at_block_height: Option<BlockHeight>,
68 #[cfg_attr(
70 feature = "serde-conversion",
71 serde(rename = "$updatedAtBlockHeight", default)
72 )]
73 pub updated_at_block_height: Option<BlockHeight>,
74 #[cfg_attr(
76 feature = "serde-conversion",
77 serde(rename = "$transferredAtBlockHeight", default)
78 )]
79 pub transferred_at_block_height: Option<BlockHeight>,
80 #[cfg_attr(
82 feature = "serde-conversion",
83 serde(rename = "$createdAtCoreBlockHeight", default)
84 )]
85 pub created_at_core_block_height: Option<CoreBlockHeight>,
86 #[cfg_attr(
88 feature = "serde-conversion",
89 serde(rename = "$updatedAtCoreBlockHeight", default)
90 )]
91 pub updated_at_core_block_height: Option<CoreBlockHeight>,
92 #[cfg_attr(
94 feature = "serde-conversion",
95 serde(rename = "$transferredAtCoreBlockHeight", default)
96 )]
97 pub transferred_at_core_block_height: Option<CoreBlockHeight>,
98 #[cfg_attr(feature = "serde-conversion", serde(rename = "$creatorId", default))]
100 pub creator_id: Option<Identifier>,
101 #[cfg_attr(
105 feature = "serde-conversion",
106 serde(
107 rename = "$moderatedAt",
108 default,
109 skip_serializing_if = "Option::is_none"
110 )
111 )]
112 pub moderated_at: Option<TimestampMillis>,
113 #[cfg_attr(
115 feature = "serde-conversion",
116 serde(
117 rename = "$moderatedBy",
118 default,
119 skip_serializing_if = "Option::is_none"
120 )
121 )]
122 pub moderated_by: Option<Identifier>,
123 #[cfg_attr(
129 feature = "serde-conversion",
130 serde(
131 rename = "$contractVersion",
132 default,
133 skip_serializing_if = "Option::is_none"
134 )
135 )]
136 pub contract_version: Option<u32>,
137}
138
139impl DocumentGetRawForContractV0 for DocumentV0 {
140 }
142
143impl DocumentIsEqualIgnoringTimestampsV0 for DocumentV0 {
144 }
146
147impl DocumentGetRawForDocumentTypeV0 for DocumentV0 {
148 }
150
151impl DocumentHashV0Method for DocumentV0 {
152 }
154
155impl fmt::Display for DocumentV0 {
156 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
157 write!(f, "id:{} ", self.id)?;
158 write!(f, "owner_id:{} ", self.owner_id)?;
159 if let Some(created_at) = self.created_at {
160 let datetime = DateTime::from_timestamp_millis(created_at as i64).unwrap_or_default();
161 write!(f, "created_at:{} ", datetime.format("%Y-%m-%d %H:%M:%S"))?;
162 }
163 if let Some(updated_at) = self.updated_at {
164 let datetime = DateTime::from_timestamp_millis(updated_at as i64).unwrap_or_default();
165 write!(f, "updated_at:{} ", datetime.format("%Y-%m-%d %H:%M:%S"))?;
166 }
167 if let Some(transferred_at) = self.transferred_at {
168 let datetime =
169 DateTime::from_timestamp_millis(transferred_at as i64).unwrap_or_default();
170 write!(
171 f,
172 "transferred_at:{} ",
173 datetime.format("%Y-%m-%d %H:%M:%S")
174 )?;
175 }
176
177 if let Some(created_at_block_height) = self.created_at_block_height {
178 write!(f, "created_at_block_height:{} ", created_at_block_height)?;
179 }
180 if let Some(updated_at_block_height) = self.updated_at_block_height {
181 write!(f, "updated_at_block_height:{} ", updated_at_block_height)?;
182 }
183 if let Some(transferred_at_block_height) = self.transferred_at_block_height {
184 write!(
185 f,
186 "transferred_at_block_height:{} ",
187 transferred_at_block_height
188 )?;
189 }
190 if let Some(created_at_core_block_height) = self.created_at_core_block_height {
191 write!(
192 f,
193 "created_at_core_block_height:{} ",
194 created_at_core_block_height
195 )?;
196 }
197 if let Some(updated_at_core_block_height) = self.updated_at_core_block_height {
198 write!(
199 f,
200 "updated_at_core_block_height:{} ",
201 updated_at_core_block_height
202 )?;
203 }
204 if let Some(transferred_at_core_block_height) = self.transferred_at_core_block_height {
205 write!(
206 f,
207 "transferred_at_core_block_height:{} ",
208 transferred_at_core_block_height
209 )?;
210 }
211
212 if let Some(creator_id) = self.creator_id {
213 write!(f, "creator_id:{} ", creator_id)?;
214 }
215 if let Some(moderated_at) = self.moderated_at {
216 let datetime = DateTime::from_timestamp_millis(moderated_at as i64).unwrap_or_default();
217 write!(f, "moderated_at:{} ", datetime.format("%Y-%m-%d %H:%M:%S"))?;
218 }
219 if let Some(moderated_by) = self.moderated_by {
220 write!(f, "moderated_by:{} ", moderated_by)?;
221 }
222
223 if self.properties.is_empty() {
224 write!(f, "no properties")?;
225 } else {
226 for (key, value) in self.properties.iter() {
227 write!(f, "{}:{} ", key, value)?
228 }
229 }
230 Ok(())
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::data_contract::accessors::v0::DataContractV0Getters;
238 use crate::document::{DocumentV0Getters, DocumentV0Setters};
239 use platform_value::Identifier;
240
241 fn minimal_doc() -> DocumentV0 {
242 DocumentV0 {
243 contract_version: None,
244 id: Identifier::new([1u8; 32]),
245 owner_id: Identifier::new([2u8; 32]),
246 properties: BTreeMap::new(),
247 revision: None,
248 created_at: None,
249 updated_at: None,
250 transferred_at: None,
251 created_at_block_height: None,
252 updated_at_block_height: None,
253 transferred_at_block_height: None,
254 created_at_core_block_height: None,
255 updated_at_core_block_height: None,
256 transferred_at_core_block_height: None,
257 creator_id: None,
258 moderated_at: None,
259 moderated_by: None,
260 }
261 }
262
263 #[test]
268 fn display_minimal_document_has_no_properties_marker() {
269 let doc = minimal_doc();
270 let s = format!("{}", doc);
271 assert!(s.contains("id:"), "should contain id");
272 assert!(s.contains("owner_id:"), "should contain owner_id");
273 assert!(
274 s.contains("no properties"),
275 "empty properties should render as 'no properties', got: {s}"
276 );
277 }
278
279 #[test]
280 fn display_with_properties_formats_key_value_pairs() {
281 let mut doc = minimal_doc();
282 doc.properties
283 .insert("name".to_string(), Value::Text("Bob".to_string()));
284 let s = format!("{}", doc);
285 assert!(!s.contains("no properties"));
286 assert!(s.contains("name:"), "should contain property key");
287 }
288
289 #[test]
290 fn display_formats_all_optional_timestamp_fields() {
291 let mut doc = minimal_doc();
292 doc.created_at = Some(1_700_000_000_000);
294 doc.updated_at = Some(1_700_000_100_000);
295 doc.transferred_at = Some(1_700_000_200_000);
296 doc.created_at_block_height = Some(10);
297 doc.updated_at_block_height = Some(20);
298 doc.transferred_at_block_height = Some(30);
299 doc.created_at_core_block_height = Some(1);
300 doc.updated_at_core_block_height = Some(2);
301 doc.transferred_at_core_block_height = Some(3);
302 doc.creator_id = Some(Identifier::new([9u8; 32]));
303
304 let s = format!("{}", doc);
305 assert!(s.contains("created_at:"), "missing created_at: {s}");
307 assert!(s.contains("updated_at:"), "missing updated_at: {s}");
308 assert!(s.contains("transferred_at:"), "missing transferred_at: {s}");
309 assert!(
310 s.contains("created_at_block_height:10"),
311 "missing created_at_block_height: {s}"
312 );
313 assert!(
314 s.contains("updated_at_block_height:20"),
315 "missing updated_at_block_height: {s}"
316 );
317 assert!(
318 s.contains("transferred_at_block_height:30"),
319 "missing transferred_at_block_height: {s}"
320 );
321 assert!(
322 s.contains("created_at_core_block_height:1"),
323 "missing created_at_core_block_height: {s}"
324 );
325 assert!(
326 s.contains("updated_at_core_block_height:2"),
327 "missing updated_at_core_block_height: {s}"
328 );
329 assert!(
330 s.contains("transferred_at_core_block_height:3"),
331 "missing transferred_at_core_block_height: {s}"
332 );
333 assert!(s.contains("creator_id:"), "missing creator_id: {s}");
334 }
335
336 #[test]
337 fn display_invalid_timestamp_uses_default_formatter() {
338 let mut doc = minimal_doc();
341 doc.created_at = Some(i64::MAX as u64);
346 let s = format!("{}", doc);
347 assert!(s.contains("created_at:"));
349 }
350
351 #[test]
356 fn bump_revision_increments_when_some() {
357 let mut doc = minimal_doc();
358 doc.set_revision(Some(5));
359 doc.bump_revision();
360 assert_eq!(doc.revision(), Some(6));
361 }
362
363 #[test]
364 fn bump_revision_is_noop_when_none() {
365 let mut doc = minimal_doc();
366 assert_eq!(doc.revision(), None);
367 doc.bump_revision();
368 assert_eq!(doc.revision(), None);
370 }
371
372 #[test]
373 fn bump_revision_saturates_at_max() {
374 let mut doc = minimal_doc();
375 doc.set_revision(Some(Revision::MAX));
376 doc.bump_revision();
377 assert_eq!(doc.revision(), Some(Revision::MAX));
379 }
380
381 #[test]
386 fn default_document_has_zero_identifiers_and_none_fields() {
387 let doc = DocumentV0::default();
388 assert_eq!(doc.id, Identifier::new([0u8; 32]));
389 assert_eq!(doc.owner_id, Identifier::new([0u8; 32]));
390 assert!(doc.properties.is_empty());
391 assert_eq!(doc.revision, None);
392 assert_eq!(doc.created_at, None);
393 assert_eq!(doc.updated_at, None);
394 assert_eq!(doc.transferred_at, None);
395 assert_eq!(doc.creator_id, None);
396 }
397
398 #[test]
403 fn documents_with_different_creator_id_are_not_equal() {
404 let a = minimal_doc();
405 let mut b = minimal_doc();
406 b.creator_id = Some(Identifier::new([7u8; 32]));
407 assert_ne!(a, b);
408 }
409
410 #[test]
411 fn documents_with_equal_fields_are_equal() {
412 let a = minimal_doc();
413 let b = minimal_doc();
414 assert_eq!(a, b);
415 }
416
417 #[test]
418 fn clone_produces_equal_document() {
419 let mut doc = minimal_doc();
420 doc.properties.insert("k".to_string(), Value::U64(42));
421 doc.revision = Some(3);
422 let cloned = doc.clone();
423 assert_eq!(doc, cloned);
424 }
425
426 #[test]
431 fn display_writes_properties_in_btreemap_sorted_order() {
432 let mut doc = minimal_doc();
437 doc.properties
438 .insert("zebra".to_string(), Value::Text("z".into()));
439 doc.properties
440 .insert("apple".to_string(), Value::Text("a".into()));
441 doc.properties
442 .insert("mango".to_string(), Value::Text("m".into()));
443
444 let s = format!("{}", doc);
445 let apple_idx = s.find("apple:").expect("apple missing");
446 let mango_idx = s.find("mango:").expect("mango missing");
447 let zebra_idx = s.find("zebra:").expect("zebra missing");
448 assert!(
449 apple_idx < mango_idx && mango_idx < zebra_idx,
450 "properties should appear in sorted (BTreeMap) order: {s}"
451 );
452 }
453
454 #[test]
455 fn display_mixes_system_fields_and_user_properties() {
456 let mut doc = minimal_doc();
461 doc.revision = Some(42);
462 doc.created_at_block_height = Some(7);
463 doc.properties
464 .insert("greeting".to_string(), Value::Text("hi".into()));
465
466 let s = format!("{}", doc);
467 assert!(s.contains("created_at_block_height:7"));
468 assert!(s.contains("greeting:"));
469 assert!(!s.contains("revision"));
472 }
473
474 #[test]
481 fn hash_v0_produces_deterministic_output_for_identical_documents() {
482 use crate::document::document_methods::DocumentHashV0Method;
483 use crate::document::serialization_traits::DocumentPlatformConversionMethodsV0;
484 use crate::tests::json_document::json_document_to_contract;
485 use platform_version::version::PlatformVersion;
486
487 let platform_version = PlatformVersion::first();
490 let contract = json_document_to_contract(
491 "../rs-drive/tests/supporting_files/contract/family/family-contract.json",
492 false,
493 platform_version,
494 )
495 .expect("expected to load family contract");
496 let doc_type = contract
497 .document_type_for_name("person")
498 .expect("expected person type");
499
500 use crate::data_contract::document_type::random_document::CreateRandomDocument;
502 let document = doc_type
503 .random_document(Some(7), platform_version)
504 .expect("random document");
505 let doc_v0 = match &document {
506 crate::document::Document::V0(d) => d.clone(),
507 };
508
509 let h1 = doc_v0
511 .hash_v0(&contract, doc_type, platform_version)
512 .expect("hash succeeds");
513 let h2 = doc_v0
514 .hash_v0(&contract, doc_type, platform_version)
515 .expect("hash succeeds");
516 assert_eq!(h1, h2);
517 assert_eq!(h1.len(), 32);
519
520 let serialized = doc_v0
523 .serialize(doc_type, &contract, platform_version)
524 .expect("serialize");
525 assert_ne!(h1, serialized);
526 }
527
528 #[test]
529 fn hash_v0_differs_between_different_documents() {
530 use crate::document::document_methods::DocumentHashV0Method;
531 use crate::tests::json_document::json_document_to_contract;
532 use platform_version::version::PlatformVersion;
533
534 let platform_version = PlatformVersion::first();
535 let contract = json_document_to_contract(
536 "../rs-drive/tests/supporting_files/contract/family/family-contract.json",
537 false,
538 platform_version,
539 )
540 .expect("family contract");
541 let doc_type = contract
542 .document_type_for_name("person")
543 .expect("person type");
544
545 use crate::data_contract::document_type::random_document::CreateRandomDocument;
546 let crate::document::Document::V0(doc_a) = doc_type
547 .random_document(Some(1), platform_version)
548 .expect("random a");
549 let crate::document::Document::V0(doc_b) = doc_type
550 .random_document(Some(2), platform_version)
551 .expect("random b");
552
553 let h_a = doc_a
554 .hash_v0(&contract, doc_type, platform_version)
555 .expect("hash a");
556 let h_b = doc_b
557 .hash_v0(&contract, doc_type, platform_version)
558 .expect("hash b");
559 assert_ne!(h_a, h_b);
560 }
561
562 #[test]
568 fn not_equal_when_revision_differs() {
569 let a = minimal_doc();
570 let mut b = minimal_doc();
571 b.revision = Some(1);
572 assert_ne!(a, b);
573 }
574
575 #[test]
576 fn not_equal_when_each_timestamp_differs() {
577 let a = minimal_doc();
578
579 let mut b = minimal_doc();
580 b.created_at = Some(1);
581 assert_ne!(a, b);
582
583 let mut b = minimal_doc();
584 b.updated_at = Some(2);
585 assert_ne!(a, b);
586
587 let mut b = minimal_doc();
588 b.transferred_at = Some(3);
589 assert_ne!(a, b);
590
591 let mut b = minimal_doc();
592 b.created_at_block_height = Some(4);
593 assert_ne!(a, b);
594
595 let mut b = minimal_doc();
596 b.updated_at_block_height = Some(5);
597 assert_ne!(a, b);
598
599 let mut b = minimal_doc();
600 b.transferred_at_block_height = Some(6);
601 assert_ne!(a, b);
602
603 let mut b = minimal_doc();
604 b.created_at_core_block_height = Some(7);
605 assert_ne!(a, b);
606
607 let mut b = minimal_doc();
608 b.updated_at_core_block_height = Some(8);
609 assert_ne!(a, b);
610
611 let mut b = minimal_doc();
612 b.transferred_at_core_block_height = Some(9);
613 assert_ne!(a, b);
614 }
615
616 #[test]
617 fn not_equal_when_properties_differ() {
618 let a = minimal_doc();
619 let mut b = minimal_doc();
620 b.properties.insert("foo".to_string(), Value::U64(1));
621 assert_ne!(a, b);
622 }
623
624 #[test]
625 fn not_equal_when_id_differs() {
626 let a = minimal_doc();
627 let mut b = minimal_doc();
628 b.id = Identifier::new([99u8; 32]);
629 assert_ne!(a, b);
630 }
631
632 #[test]
633 fn not_equal_when_owner_id_differs() {
634 let a = minimal_doc();
635 let mut b = minimal_doc();
636 b.owner_id = Identifier::new([98u8; 32]);
637 assert_ne!(a, b);
638 }
639
640 #[test]
646 fn bump_revision_from_zero_increments_to_one() {
647 let mut doc = minimal_doc();
648 doc.set_revision(Some(0));
649 doc.bump_revision();
650 assert_eq!(doc.revision(), Some(1));
651 }
652
653 #[test]
654 fn bump_revision_from_max_minus_one_reaches_max_then_saturates() {
655 let mut doc = minimal_doc();
656 doc.set_revision(Some(Revision::MAX - 1));
657 doc.bump_revision();
658 assert_eq!(doc.revision(), Some(Revision::MAX));
659 doc.bump_revision();
660 assert_eq!(doc.revision(), Some(Revision::MAX));
661 doc.bump_revision();
663 assert_eq!(doc.revision(), Some(Revision::MAX));
664 }
665
666 #[test]
672 fn setters_round_trip_every_field() {
673 use crate::document::{DocumentV0Getters, DocumentV0Setters};
674 let mut doc = DocumentV0::default();
675 doc.set_id(Identifier::new([1u8; 32]));
676 doc.set_owner_id(Identifier::new([2u8; 32]));
677 let mut props = BTreeMap::new();
678 props.insert("a".to_string(), Value::U64(99));
679 doc.set_properties(props.clone());
680 doc.set_revision(Some(4));
681 doc.set_created_at(Some(10));
682 doc.set_updated_at(Some(20));
683 doc.set_transferred_at(Some(30));
684 doc.set_created_at_block_height(Some(100));
685 doc.set_updated_at_block_height(Some(200));
686 doc.set_transferred_at_block_height(Some(300));
687 doc.set_created_at_core_block_height(Some(1));
688 doc.set_updated_at_core_block_height(Some(2));
689 doc.set_transferred_at_core_block_height(Some(3));
690 doc.set_creator_id(Some(Identifier::new([9u8; 32])));
691
692 assert_eq!(doc.id(), Identifier::new([1u8; 32]));
693 assert_eq!(doc.owner_id(), Identifier::new([2u8; 32]));
694 assert_eq!(doc.properties(), &props);
695 assert_eq!(doc.revision(), Some(4));
696 assert_eq!(doc.created_at(), Some(10));
697 assert_eq!(doc.updated_at(), Some(20));
698 assert_eq!(doc.transferred_at(), Some(30));
699 assert_eq!(doc.created_at_block_height(), Some(100));
700 assert_eq!(doc.updated_at_block_height(), Some(200));
701 assert_eq!(doc.transferred_at_block_height(), Some(300));
702 assert_eq!(doc.created_at_core_block_height(), Some(1));
703 assert_eq!(doc.updated_at_core_block_height(), Some(2));
704 assert_eq!(doc.transferred_at_core_block_height(), Some(3));
705 assert_eq!(doc.creator_id(), Some(Identifier::new([9u8; 32])));
706
707 assert_eq!(doc.id_ref(), &Identifier::new([1u8; 32]));
710 assert_eq!(doc.owner_id_ref(), &Identifier::new([2u8; 32]));
711 assert_eq!(doc.clone().properties_consumed(), props);
712 }
713
714 #[test]
720 fn properties_mut_allows_inserting_new_key() {
721 use crate::document::DocumentV0Getters;
722 let mut doc = minimal_doc();
723 doc.properties_mut().insert("k".into(), Value::U64(7));
724 assert_eq!(doc.properties().get("k"), Some(&Value::U64(7)));
725 }
726
727 #[test]
734 fn debug_format_contains_field_names() {
735 let doc = minimal_doc();
736 let dbg = format!("{:?}", doc);
737 assert!(dbg.contains("DocumentV0"), "expected struct name in Debug");
738 assert!(dbg.contains("id"));
739 assert!(dbg.contains("owner_id"));
740 }
741
742 #[test]
749 fn display_with_only_creator_id_and_no_timestamps() {
750 let mut doc = minimal_doc();
751 doc.creator_id = Some(Identifier::new([7u8; 32]));
752 let s = format!("{}", doc);
753 assert!(s.contains("creator_id:"));
754 assert!(!s.contains("created_at:"));
756 assert!(!s.contains("updated_at:"));
757 assert!(!s.contains("transferred_at:"));
758 assert!(s.contains("no properties"));
760 }
761}