1mod accessors;
7#[cfg(feature = "document-cbor-conversion")]
8pub(super) mod cbor_conversion;
9#[cfg(feature = "json-conversion")]
10pub(super) mod json_conversion;
11#[cfg(feature = "value-conversion")]
12mod platform_value_conversion;
13pub mod serialize;
14
15use chrono::DateTime;
16use std::collections::BTreeMap;
17use std::fmt;
18
19use platform_value::Value;
20
21use crate::document::document_methods::{
22 DocumentGetRawForContractV0, DocumentGetRawForDocumentTypeV0, DocumentHashV0Method,
23 DocumentIsEqualIgnoringTimestampsV0,
24};
25
26use crate::identity::TimestampMillis;
27use crate::prelude::Revision;
28use crate::prelude::{BlockHeight, CoreBlockHeight, Identifier};
29#[cfg(feature = "json-conversion")]
30use crate::serialization::json_safe_fields;
31
32#[cfg_attr(feature = "json-conversion", json_safe_fields)]
34#[derive(Clone, Debug, PartialEq, Default)]
35#[cfg_attr(
36 any(feature = "serde-conversion", feature = "serde-conversion"),
37 derive(serde::Serialize, serde::Deserialize)
38)]
39pub struct DocumentV0 {
40 #[cfg_attr(
42 any(feature = "serde-conversion", feature = "serde-conversion"),
43 serde(rename = "$id")
44 )]
45 pub id: Identifier,
46 #[cfg_attr(
48 any(feature = "serde-conversion", feature = "serde-conversion"),
49 serde(rename = "$ownerId")
50 )]
51 pub owner_id: Identifier,
52 #[cfg_attr(
54 any(feature = "serde-conversion", feature = "serde-conversion"),
55 serde(flatten)
56 )]
57 pub properties: BTreeMap<String, Value>,
58 #[cfg_attr(
60 any(feature = "serde-conversion", feature = "serde-conversion"),
61 serde(rename = "$revision", default)
62 )]
63 pub revision: Option<Revision>,
64 #[cfg_attr(
66 any(feature = "serde-conversion", feature = "serde-conversion"),
67 serde(rename = "$createdAt", default)
68 )]
69 pub created_at: Option<TimestampMillis>,
70 #[cfg_attr(
72 any(feature = "serde-conversion", feature = "serde-conversion"),
73 serde(rename = "$updatedAt", default)
74 )]
75 pub updated_at: Option<TimestampMillis>,
76 #[cfg_attr(
78 any(feature = "serde-conversion", feature = "serde-conversion"),
79 serde(rename = "$transferredAt", default)
80 )]
81 pub transferred_at: Option<TimestampMillis>,
82 #[cfg_attr(
84 any(feature = "serde-conversion", feature = "serde-conversion"),
85 serde(rename = "$createdAtBlockHeight", default)
86 )]
87 pub created_at_block_height: Option<BlockHeight>,
88 #[cfg_attr(
90 any(feature = "serde-conversion", feature = "serde-conversion"),
91 serde(rename = "$updatedAtBlockHeight", default)
92 )]
93 pub updated_at_block_height: Option<BlockHeight>,
94 #[cfg_attr(
96 any(feature = "serde-conversion", feature = "serde-conversion"),
97 serde(rename = "$transferredAtBlockHeight", default)
98 )]
99 pub transferred_at_block_height: Option<BlockHeight>,
100 #[cfg_attr(
102 any(feature = "serde-conversion", feature = "serde-conversion"),
103 serde(rename = "$createdAtCoreBlockHeight", default)
104 )]
105 pub created_at_core_block_height: Option<CoreBlockHeight>,
106 #[cfg_attr(
108 any(feature = "serde-conversion", feature = "serde-conversion"),
109 serde(rename = "$updatedAtCoreBlockHeight", default)
110 )]
111 pub updated_at_core_block_height: Option<CoreBlockHeight>,
112 #[cfg_attr(
114 any(feature = "serde-conversion", feature = "serde-conversion"),
115 serde(rename = "$transferredAtCoreBlockHeight", default)
116 )]
117 pub transferred_at_core_block_height: Option<CoreBlockHeight>,
118 #[cfg_attr(
120 any(feature = "serde-conversion", feature = "serde-conversion"),
121 serde(rename = "$creatorId", default)
122 )]
123 pub creator_id: Option<Identifier>,
124}
125
126impl DocumentGetRawForContractV0 for DocumentV0 {
127 }
129
130impl DocumentIsEqualIgnoringTimestampsV0 for DocumentV0 {
131 }
133
134impl DocumentGetRawForDocumentTypeV0 for DocumentV0 {
135 }
137
138impl DocumentHashV0Method for DocumentV0 {
139 }
141
142impl fmt::Display for DocumentV0 {
143 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
144 write!(f, "id:{} ", self.id)?;
145 write!(f, "owner_id:{} ", self.owner_id)?;
146 if let Some(created_at) = self.created_at {
147 let datetime = DateTime::from_timestamp_millis(created_at as i64).unwrap_or_default();
148 write!(f, "created_at:{} ", datetime.format("%Y-%m-%d %H:%M:%S"))?;
149 }
150 if let Some(updated_at) = self.updated_at {
151 let datetime = DateTime::from_timestamp_millis(updated_at as i64).unwrap_or_default();
152 write!(f, "updated_at:{} ", datetime.format("%Y-%m-%d %H:%M:%S"))?;
153 }
154 if let Some(transferred_at) = self.transferred_at {
155 let datetime =
156 DateTime::from_timestamp_millis(transferred_at as i64).unwrap_or_default();
157 write!(
158 f,
159 "transferred_at:{} ",
160 datetime.format("%Y-%m-%d %H:%M:%S")
161 )?;
162 }
163
164 if let Some(created_at_block_height) = self.created_at_block_height {
165 write!(f, "created_at_block_height:{} ", created_at_block_height)?;
166 }
167 if let Some(updated_at_block_height) = self.updated_at_block_height {
168 write!(f, "updated_at_block_height:{} ", updated_at_block_height)?;
169 }
170 if let Some(transferred_at_block_height) = self.transferred_at_block_height {
171 write!(
172 f,
173 "transferred_at_block_height:{} ",
174 transferred_at_block_height
175 )?;
176 }
177 if let Some(created_at_core_block_height) = self.created_at_core_block_height {
178 write!(
179 f,
180 "created_at_core_block_height:{} ",
181 created_at_core_block_height
182 )?;
183 }
184 if let Some(updated_at_core_block_height) = self.updated_at_core_block_height {
185 write!(
186 f,
187 "updated_at_core_block_height:{} ",
188 updated_at_core_block_height
189 )?;
190 }
191 if let Some(transferred_at_core_block_height) = self.transferred_at_core_block_height {
192 write!(
193 f,
194 "transferred_at_core_block_height:{} ",
195 transferred_at_core_block_height
196 )?;
197 }
198
199 if let Some(creator_id) = self.creator_id {
200 write!(f, "creator_id:{} ", creator_id)?;
201 }
202
203 if self.properties.is_empty() {
204 write!(f, "no properties")?;
205 } else {
206 for (key, value) in self.properties.iter() {
207 write!(f, "{}:{} ", key, value)?
208 }
209 }
210 Ok(())
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::data_contract::accessors::v0::DataContractV0Getters;
218 use crate::document::{DocumentV0Getters, DocumentV0Setters};
219 use platform_value::Identifier;
220
221 fn minimal_doc() -> DocumentV0 {
222 DocumentV0 {
223 id: Identifier::new([1u8; 32]),
224 owner_id: Identifier::new([2u8; 32]),
225 properties: BTreeMap::new(),
226 revision: None,
227 created_at: None,
228 updated_at: None,
229 transferred_at: None,
230 created_at_block_height: None,
231 updated_at_block_height: None,
232 transferred_at_block_height: None,
233 created_at_core_block_height: None,
234 updated_at_core_block_height: None,
235 transferred_at_core_block_height: None,
236 creator_id: None,
237 }
238 }
239
240 #[test]
245 fn display_minimal_document_has_no_properties_marker() {
246 let doc = minimal_doc();
247 let s = format!("{}", doc);
248 assert!(s.contains("id:"), "should contain id");
249 assert!(s.contains("owner_id:"), "should contain owner_id");
250 assert!(
251 s.contains("no properties"),
252 "empty properties should render as 'no properties', got: {s}"
253 );
254 }
255
256 #[test]
257 fn display_with_properties_formats_key_value_pairs() {
258 let mut doc = minimal_doc();
259 doc.properties
260 .insert("name".to_string(), Value::Text("Bob".to_string()));
261 let s = format!("{}", doc);
262 assert!(!s.contains("no properties"));
263 assert!(s.contains("name:"), "should contain property key");
264 }
265
266 #[test]
267 fn display_formats_all_optional_timestamp_fields() {
268 let mut doc = minimal_doc();
269 doc.created_at = Some(1_700_000_000_000);
271 doc.updated_at = Some(1_700_000_100_000);
272 doc.transferred_at = Some(1_700_000_200_000);
273 doc.created_at_block_height = Some(10);
274 doc.updated_at_block_height = Some(20);
275 doc.transferred_at_block_height = Some(30);
276 doc.created_at_core_block_height = Some(1);
277 doc.updated_at_core_block_height = Some(2);
278 doc.transferred_at_core_block_height = Some(3);
279 doc.creator_id = Some(Identifier::new([9u8; 32]));
280
281 let s = format!("{}", doc);
282 assert!(s.contains("created_at:"), "missing created_at: {s}");
284 assert!(s.contains("updated_at:"), "missing updated_at: {s}");
285 assert!(s.contains("transferred_at:"), "missing transferred_at: {s}");
286 assert!(
287 s.contains("created_at_block_height:10"),
288 "missing created_at_block_height: {s}"
289 );
290 assert!(
291 s.contains("updated_at_block_height:20"),
292 "missing updated_at_block_height: {s}"
293 );
294 assert!(
295 s.contains("transferred_at_block_height:30"),
296 "missing transferred_at_block_height: {s}"
297 );
298 assert!(
299 s.contains("created_at_core_block_height:1"),
300 "missing created_at_core_block_height: {s}"
301 );
302 assert!(
303 s.contains("updated_at_core_block_height:2"),
304 "missing updated_at_core_block_height: {s}"
305 );
306 assert!(
307 s.contains("transferred_at_core_block_height:3"),
308 "missing transferred_at_core_block_height: {s}"
309 );
310 assert!(s.contains("creator_id:"), "missing creator_id: {s}");
311 }
312
313 #[test]
314 fn display_invalid_timestamp_uses_default_formatter() {
315 let mut doc = minimal_doc();
318 doc.created_at = Some(i64::MAX as u64);
323 let s = format!("{}", doc);
324 assert!(s.contains("created_at:"));
326 }
327
328 #[test]
333 fn bump_revision_increments_when_some() {
334 let mut doc = minimal_doc();
335 doc.set_revision(Some(5));
336 doc.bump_revision();
337 assert_eq!(doc.revision(), Some(6));
338 }
339
340 #[test]
341 fn bump_revision_is_noop_when_none() {
342 let mut doc = minimal_doc();
343 assert_eq!(doc.revision(), None);
344 doc.bump_revision();
345 assert_eq!(doc.revision(), None);
347 }
348
349 #[test]
350 fn bump_revision_saturates_at_max() {
351 let mut doc = minimal_doc();
352 doc.set_revision(Some(Revision::MAX));
353 doc.bump_revision();
354 assert_eq!(doc.revision(), Some(Revision::MAX));
356 }
357
358 #[test]
363 fn default_document_has_zero_identifiers_and_none_fields() {
364 let doc = DocumentV0::default();
365 assert_eq!(doc.id, Identifier::new([0u8; 32]));
366 assert_eq!(doc.owner_id, Identifier::new([0u8; 32]));
367 assert!(doc.properties.is_empty());
368 assert_eq!(doc.revision, None);
369 assert_eq!(doc.created_at, None);
370 assert_eq!(doc.updated_at, None);
371 assert_eq!(doc.transferred_at, None);
372 assert_eq!(doc.creator_id, None);
373 }
374
375 #[test]
380 fn documents_with_different_creator_id_are_not_equal() {
381 let a = minimal_doc();
382 let mut b = minimal_doc();
383 b.creator_id = Some(Identifier::new([7u8; 32]));
384 assert_ne!(a, b);
385 }
386
387 #[test]
388 fn documents_with_equal_fields_are_equal() {
389 let a = minimal_doc();
390 let b = minimal_doc();
391 assert_eq!(a, b);
392 }
393
394 #[test]
395 fn clone_produces_equal_document() {
396 let mut doc = minimal_doc();
397 doc.properties.insert("k".to_string(), Value::U64(42));
398 doc.revision = Some(3);
399 let cloned = doc.clone();
400 assert_eq!(doc, cloned);
401 }
402
403 #[test]
408 fn display_writes_properties_in_btreemap_sorted_order() {
409 let mut doc = minimal_doc();
414 doc.properties
415 .insert("zebra".to_string(), Value::Text("z".into()));
416 doc.properties
417 .insert("apple".to_string(), Value::Text("a".into()));
418 doc.properties
419 .insert("mango".to_string(), Value::Text("m".into()));
420
421 let s = format!("{}", doc);
422 let apple_idx = s.find("apple:").expect("apple missing");
423 let mango_idx = s.find("mango:").expect("mango missing");
424 let zebra_idx = s.find("zebra:").expect("zebra missing");
425 assert!(
426 apple_idx < mango_idx && mango_idx < zebra_idx,
427 "properties should appear in sorted (BTreeMap) order: {s}"
428 );
429 }
430
431 #[test]
432 fn display_mixes_system_fields_and_user_properties() {
433 let mut doc = minimal_doc();
438 doc.revision = Some(42);
439 doc.created_at_block_height = Some(7);
440 doc.properties
441 .insert("greeting".to_string(), Value::Text("hi".into()));
442
443 let s = format!("{}", doc);
444 assert!(s.contains("created_at_block_height:7"));
445 assert!(s.contains("greeting:"));
446 assert!(!s.contains("revision"));
449 }
450
451 #[test]
458 fn hash_v0_produces_deterministic_output_for_identical_documents() {
459 use crate::document::document_methods::DocumentHashV0Method;
460 use crate::document::serialization_traits::DocumentPlatformConversionMethodsV0;
461 use crate::tests::json_document::json_document_to_contract;
462 use platform_version::version::PlatformVersion;
463
464 let platform_version = PlatformVersion::first();
467 let contract = json_document_to_contract(
468 "../rs-drive/tests/supporting_files/contract/family/family-contract.json",
469 false,
470 platform_version,
471 )
472 .expect("expected to load family contract");
473 let doc_type = contract
474 .document_type_for_name("person")
475 .expect("expected person type");
476
477 use crate::data_contract::document_type::random_document::CreateRandomDocument;
479 let document = doc_type
480 .random_document(Some(7), platform_version)
481 .expect("random document");
482 let doc_v0 = match &document {
483 crate::document::Document::V0(d) => d.clone(),
484 };
485
486 let h1 = doc_v0
488 .hash_v0(&contract, doc_type, platform_version)
489 .expect("hash succeeds");
490 let h2 = doc_v0
491 .hash_v0(&contract, doc_type, platform_version)
492 .expect("hash succeeds");
493 assert_eq!(h1, h2);
494 assert_eq!(h1.len(), 32);
496
497 let serialized = doc_v0
500 .serialize(doc_type, &contract, platform_version)
501 .expect("serialize");
502 assert_ne!(h1, serialized);
503 }
504
505 #[test]
506 fn hash_v0_differs_between_different_documents() {
507 use crate::document::document_methods::DocumentHashV0Method;
508 use crate::tests::json_document::json_document_to_contract;
509 use platform_version::version::PlatformVersion;
510
511 let platform_version = PlatformVersion::first();
512 let contract = json_document_to_contract(
513 "../rs-drive/tests/supporting_files/contract/family/family-contract.json",
514 false,
515 platform_version,
516 )
517 .expect("family contract");
518 let doc_type = contract
519 .document_type_for_name("person")
520 .expect("person type");
521
522 use crate::data_contract::document_type::random_document::CreateRandomDocument;
523 let crate::document::Document::V0(doc_a) = doc_type
524 .random_document(Some(1), platform_version)
525 .expect("random a");
526 let crate::document::Document::V0(doc_b) = doc_type
527 .random_document(Some(2), platform_version)
528 .expect("random b");
529
530 let h_a = doc_a
531 .hash_v0(&contract, doc_type, platform_version)
532 .expect("hash a");
533 let h_b = doc_b
534 .hash_v0(&contract, doc_type, platform_version)
535 .expect("hash b");
536 assert_ne!(h_a, h_b);
537 }
538
539 #[test]
545 fn not_equal_when_revision_differs() {
546 let a = minimal_doc();
547 let mut b = minimal_doc();
548 b.revision = Some(1);
549 assert_ne!(a, b);
550 }
551
552 #[test]
553 fn not_equal_when_each_timestamp_differs() {
554 let a = minimal_doc();
555
556 let mut b = minimal_doc();
557 b.created_at = Some(1);
558 assert_ne!(a, b);
559
560 let mut b = minimal_doc();
561 b.updated_at = Some(2);
562 assert_ne!(a, b);
563
564 let mut b = minimal_doc();
565 b.transferred_at = Some(3);
566 assert_ne!(a, b);
567
568 let mut b = minimal_doc();
569 b.created_at_block_height = Some(4);
570 assert_ne!(a, b);
571
572 let mut b = minimal_doc();
573 b.updated_at_block_height = Some(5);
574 assert_ne!(a, b);
575
576 let mut b = minimal_doc();
577 b.transferred_at_block_height = Some(6);
578 assert_ne!(a, b);
579
580 let mut b = minimal_doc();
581 b.created_at_core_block_height = Some(7);
582 assert_ne!(a, b);
583
584 let mut b = minimal_doc();
585 b.updated_at_core_block_height = Some(8);
586 assert_ne!(a, b);
587
588 let mut b = minimal_doc();
589 b.transferred_at_core_block_height = Some(9);
590 assert_ne!(a, b);
591 }
592
593 #[test]
594 fn not_equal_when_properties_differ() {
595 let a = minimal_doc();
596 let mut b = minimal_doc();
597 b.properties.insert("foo".to_string(), Value::U64(1));
598 assert_ne!(a, b);
599 }
600
601 #[test]
602 fn not_equal_when_id_differs() {
603 let a = minimal_doc();
604 let mut b = minimal_doc();
605 b.id = Identifier::new([99u8; 32]);
606 assert_ne!(a, b);
607 }
608
609 #[test]
610 fn not_equal_when_owner_id_differs() {
611 let a = minimal_doc();
612 let mut b = minimal_doc();
613 b.owner_id = Identifier::new([98u8; 32]);
614 assert_ne!(a, b);
615 }
616
617 #[test]
623 fn bump_revision_from_zero_increments_to_one() {
624 let mut doc = minimal_doc();
625 doc.set_revision(Some(0));
626 doc.bump_revision();
627 assert_eq!(doc.revision(), Some(1));
628 }
629
630 #[test]
631 fn bump_revision_from_max_minus_one_reaches_max_then_saturates() {
632 let mut doc = minimal_doc();
633 doc.set_revision(Some(Revision::MAX - 1));
634 doc.bump_revision();
635 assert_eq!(doc.revision(), Some(Revision::MAX));
636 doc.bump_revision();
637 assert_eq!(doc.revision(), Some(Revision::MAX));
638 doc.bump_revision();
640 assert_eq!(doc.revision(), Some(Revision::MAX));
641 }
642
643 #[test]
649 fn setters_round_trip_every_field() {
650 use crate::document::{DocumentV0Getters, DocumentV0Setters};
651 let mut doc = DocumentV0::default();
652 doc.set_id(Identifier::new([1u8; 32]));
653 doc.set_owner_id(Identifier::new([2u8; 32]));
654 let mut props = BTreeMap::new();
655 props.insert("a".to_string(), Value::U64(99));
656 doc.set_properties(props.clone());
657 doc.set_revision(Some(4));
658 doc.set_created_at(Some(10));
659 doc.set_updated_at(Some(20));
660 doc.set_transferred_at(Some(30));
661 doc.set_created_at_block_height(Some(100));
662 doc.set_updated_at_block_height(Some(200));
663 doc.set_transferred_at_block_height(Some(300));
664 doc.set_created_at_core_block_height(Some(1));
665 doc.set_updated_at_core_block_height(Some(2));
666 doc.set_transferred_at_core_block_height(Some(3));
667 doc.set_creator_id(Some(Identifier::new([9u8; 32])));
668
669 assert_eq!(doc.id(), Identifier::new([1u8; 32]));
670 assert_eq!(doc.owner_id(), Identifier::new([2u8; 32]));
671 assert_eq!(doc.properties(), &props);
672 assert_eq!(doc.revision(), Some(4));
673 assert_eq!(doc.created_at(), Some(10));
674 assert_eq!(doc.updated_at(), Some(20));
675 assert_eq!(doc.transferred_at(), Some(30));
676 assert_eq!(doc.created_at_block_height(), Some(100));
677 assert_eq!(doc.updated_at_block_height(), Some(200));
678 assert_eq!(doc.transferred_at_block_height(), Some(300));
679 assert_eq!(doc.created_at_core_block_height(), Some(1));
680 assert_eq!(doc.updated_at_core_block_height(), Some(2));
681 assert_eq!(doc.transferred_at_core_block_height(), Some(3));
682 assert_eq!(doc.creator_id(), Some(Identifier::new([9u8; 32])));
683
684 assert_eq!(doc.id_ref(), &Identifier::new([1u8; 32]));
687 assert_eq!(doc.owner_id_ref(), &Identifier::new([2u8; 32]));
688 assert_eq!(doc.clone().properties_consumed(), props);
689 }
690
691 #[test]
697 fn properties_mut_allows_inserting_new_key() {
698 use crate::document::DocumentV0Getters;
699 let mut doc = minimal_doc();
700 doc.properties_mut().insert("k".into(), Value::U64(7));
701 assert_eq!(doc.properties().get("k"), Some(&Value::U64(7)));
702 }
703
704 #[test]
711 fn debug_format_contains_field_names() {
712 let doc = minimal_doc();
713 let dbg = format!("{:?}", doc);
714 assert!(dbg.contains("DocumentV0"), "expected struct name in Debug");
715 assert!(dbg.contains("id"));
716 assert!(dbg.contains("owner_id"));
717 }
718
719 #[test]
726 fn display_with_only_creator_id_and_no_timestamps() {
727 let mut doc = minimal_doc();
728 doc.creator_id = Some(Identifier::new([7u8; 32]));
729 let s = format!("{}", doc);
730 assert!(s.contains("creator_id:"));
731 assert!(!s.contains("created_at:"));
733 assert!(!s.contains("updated_at:"));
734 assert!(!s.contains("transferred_at:"));
735 assert!(s.contains("no properties"));
737 }
738}