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 DocumentGetRawArrayElementsForDocumentTypeV0, DocumentGetRawForContractV0,
21 DocumentGetRawForDocumentTypeV0, DocumentHashV0Method, 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 DocumentGetRawArrayElementsForDocumentTypeV0 for DocumentV0 {
152 }
154
155impl DocumentHashV0Method for DocumentV0 {
156 }
158
159impl fmt::Display for DocumentV0 {
160 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
161 write!(f, "id:{} ", self.id)?;
162 write!(f, "owner_id:{} ", self.owner_id)?;
163 if let Some(created_at) = self.created_at {
164 let datetime = DateTime::from_timestamp_millis(created_at as i64).unwrap_or_default();
165 write!(f, "created_at:{} ", datetime.format("%Y-%m-%d %H:%M:%S"))?;
166 }
167 if let Some(updated_at) = self.updated_at {
168 let datetime = DateTime::from_timestamp_millis(updated_at as i64).unwrap_or_default();
169 write!(f, "updated_at:{} ", datetime.format("%Y-%m-%d %H:%M:%S"))?;
170 }
171 if let Some(transferred_at) = self.transferred_at {
172 let datetime =
173 DateTime::from_timestamp_millis(transferred_at as i64).unwrap_or_default();
174 write!(
175 f,
176 "transferred_at:{} ",
177 datetime.format("%Y-%m-%d %H:%M:%S")
178 )?;
179 }
180
181 if let Some(created_at_block_height) = self.created_at_block_height {
182 write!(f, "created_at_block_height:{} ", created_at_block_height)?;
183 }
184 if let Some(updated_at_block_height) = self.updated_at_block_height {
185 write!(f, "updated_at_block_height:{} ", updated_at_block_height)?;
186 }
187 if let Some(transferred_at_block_height) = self.transferred_at_block_height {
188 write!(
189 f,
190 "transferred_at_block_height:{} ",
191 transferred_at_block_height
192 )?;
193 }
194 if let Some(created_at_core_block_height) = self.created_at_core_block_height {
195 write!(
196 f,
197 "created_at_core_block_height:{} ",
198 created_at_core_block_height
199 )?;
200 }
201 if let Some(updated_at_core_block_height) = self.updated_at_core_block_height {
202 write!(
203 f,
204 "updated_at_core_block_height:{} ",
205 updated_at_core_block_height
206 )?;
207 }
208 if let Some(transferred_at_core_block_height) = self.transferred_at_core_block_height {
209 write!(
210 f,
211 "transferred_at_core_block_height:{} ",
212 transferred_at_core_block_height
213 )?;
214 }
215
216 if let Some(creator_id) = self.creator_id {
217 write!(f, "creator_id:{} ", creator_id)?;
218 }
219 if let Some(moderated_at) = self.moderated_at {
220 let datetime = DateTime::from_timestamp_millis(moderated_at as i64).unwrap_or_default();
221 write!(f, "moderated_at:{} ", datetime.format("%Y-%m-%d %H:%M:%S"))?;
222 }
223 if let Some(moderated_by) = self.moderated_by {
224 write!(f, "moderated_by:{} ", moderated_by)?;
225 }
226
227 if self.properties.is_empty() {
228 write!(f, "no properties")?;
229 } else {
230 for (key, value) in self.properties.iter() {
231 write!(f, "{}:{} ", key, value)?
232 }
233 }
234 Ok(())
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::data_contract::accessors::v0::DataContractV0Getters;
242 use crate::document::{DocumentV0Getters, DocumentV0Setters};
243 use platform_value::Identifier;
244
245 fn minimal_doc() -> DocumentV0 {
246 DocumentV0 {
247 contract_version: None,
248 id: Identifier::new([1u8; 32]),
249 owner_id: Identifier::new([2u8; 32]),
250 properties: BTreeMap::new(),
251 revision: None,
252 created_at: None,
253 updated_at: None,
254 transferred_at: None,
255 created_at_block_height: None,
256 updated_at_block_height: None,
257 transferred_at_block_height: None,
258 created_at_core_block_height: None,
259 updated_at_core_block_height: None,
260 transferred_at_core_block_height: None,
261 creator_id: None,
262 moderated_at: None,
263 moderated_by: None,
264 }
265 }
266
267 #[test]
272 fn display_minimal_document_has_no_properties_marker() {
273 let doc = minimal_doc();
274 let s = format!("{}", doc);
275 assert!(s.contains("id:"), "should contain id");
276 assert!(s.contains("owner_id:"), "should contain owner_id");
277 assert!(
278 s.contains("no properties"),
279 "empty properties should render as 'no properties', got: {s}"
280 );
281 }
282
283 #[test]
284 fn display_with_properties_formats_key_value_pairs() {
285 let mut doc = minimal_doc();
286 doc.properties
287 .insert("name".to_string(), Value::Text("Bob".to_string()));
288 let s = format!("{}", doc);
289 assert!(!s.contains("no properties"));
290 assert!(s.contains("name:"), "should contain property key");
291 }
292
293 #[test]
294 fn display_formats_all_optional_timestamp_fields() {
295 let mut doc = minimal_doc();
296 doc.created_at = Some(1_700_000_000_000);
298 doc.updated_at = Some(1_700_000_100_000);
299 doc.transferred_at = Some(1_700_000_200_000);
300 doc.created_at_block_height = Some(10);
301 doc.updated_at_block_height = Some(20);
302 doc.transferred_at_block_height = Some(30);
303 doc.created_at_core_block_height = Some(1);
304 doc.updated_at_core_block_height = Some(2);
305 doc.transferred_at_core_block_height = Some(3);
306 doc.creator_id = Some(Identifier::new([9u8; 32]));
307
308 let s = format!("{}", doc);
309 assert!(s.contains("created_at:"), "missing created_at: {s}");
311 assert!(s.contains("updated_at:"), "missing updated_at: {s}");
312 assert!(s.contains("transferred_at:"), "missing transferred_at: {s}");
313 assert!(
314 s.contains("created_at_block_height:10"),
315 "missing created_at_block_height: {s}"
316 );
317 assert!(
318 s.contains("updated_at_block_height:20"),
319 "missing updated_at_block_height: {s}"
320 );
321 assert!(
322 s.contains("transferred_at_block_height:30"),
323 "missing transferred_at_block_height: {s}"
324 );
325 assert!(
326 s.contains("created_at_core_block_height:1"),
327 "missing created_at_core_block_height: {s}"
328 );
329 assert!(
330 s.contains("updated_at_core_block_height:2"),
331 "missing updated_at_core_block_height: {s}"
332 );
333 assert!(
334 s.contains("transferred_at_core_block_height:3"),
335 "missing transferred_at_core_block_height: {s}"
336 );
337 assert!(s.contains("creator_id:"), "missing creator_id: {s}");
338 }
339
340 #[test]
341 fn display_invalid_timestamp_uses_default_formatter() {
342 let mut doc = minimal_doc();
345 doc.created_at = Some(i64::MAX as u64);
350 let s = format!("{}", doc);
351 assert!(s.contains("created_at:"));
353 }
354
355 #[test]
360 fn bump_revision_increments_when_some() {
361 let mut doc = minimal_doc();
362 doc.set_revision(Some(5));
363 doc.bump_revision();
364 assert_eq!(doc.revision(), Some(6));
365 }
366
367 #[test]
368 fn bump_revision_is_noop_when_none() {
369 let mut doc = minimal_doc();
370 assert_eq!(doc.revision(), None);
371 doc.bump_revision();
372 assert_eq!(doc.revision(), None);
374 }
375
376 #[test]
377 fn bump_revision_saturates_at_max() {
378 let mut doc = minimal_doc();
379 doc.set_revision(Some(Revision::MAX));
380 doc.bump_revision();
381 assert_eq!(doc.revision(), Some(Revision::MAX));
383 }
384
385 #[test]
390 fn default_document_has_zero_identifiers_and_none_fields() {
391 let doc = DocumentV0::default();
392 assert_eq!(doc.id, Identifier::new([0u8; 32]));
393 assert_eq!(doc.owner_id, Identifier::new([0u8; 32]));
394 assert!(doc.properties.is_empty());
395 assert_eq!(doc.revision, None);
396 assert_eq!(doc.created_at, None);
397 assert_eq!(doc.updated_at, None);
398 assert_eq!(doc.transferred_at, None);
399 assert_eq!(doc.creator_id, None);
400 }
401
402 #[test]
407 fn documents_with_different_creator_id_are_not_equal() {
408 let a = minimal_doc();
409 let mut b = minimal_doc();
410 b.creator_id = Some(Identifier::new([7u8; 32]));
411 assert_ne!(a, b);
412 }
413
414 #[test]
415 fn documents_with_equal_fields_are_equal() {
416 let a = minimal_doc();
417 let b = minimal_doc();
418 assert_eq!(a, b);
419 }
420
421 #[test]
422 fn clone_produces_equal_document() {
423 let mut doc = minimal_doc();
424 doc.properties.insert("k".to_string(), Value::U64(42));
425 doc.revision = Some(3);
426 let cloned = doc.clone();
427 assert_eq!(doc, cloned);
428 }
429
430 #[test]
435 fn display_writes_properties_in_btreemap_sorted_order() {
436 let mut doc = minimal_doc();
441 doc.properties
442 .insert("zebra".to_string(), Value::Text("z".into()));
443 doc.properties
444 .insert("apple".to_string(), Value::Text("a".into()));
445 doc.properties
446 .insert("mango".to_string(), Value::Text("m".into()));
447
448 let s = format!("{}", doc);
449 let apple_idx = s.find("apple:").expect("apple missing");
450 let mango_idx = s.find("mango:").expect("mango missing");
451 let zebra_idx = s.find("zebra:").expect("zebra missing");
452 assert!(
453 apple_idx < mango_idx && mango_idx < zebra_idx,
454 "properties should appear in sorted (BTreeMap) order: {s}"
455 );
456 }
457
458 #[test]
459 fn display_mixes_system_fields_and_user_properties() {
460 let mut doc = minimal_doc();
465 doc.revision = Some(42);
466 doc.created_at_block_height = Some(7);
467 doc.properties
468 .insert("greeting".to_string(), Value::Text("hi".into()));
469
470 let s = format!("{}", doc);
471 assert!(s.contains("created_at_block_height:7"));
472 assert!(s.contains("greeting:"));
473 assert!(!s.contains("revision"));
476 }
477
478 #[test]
485 fn hash_v0_produces_deterministic_output_for_identical_documents() {
486 use crate::document::document_methods::DocumentHashV0Method;
487 use crate::document::serialization_traits::DocumentPlatformConversionMethodsV0;
488 use crate::tests::json_document::json_document_to_contract;
489 use platform_version::version::PlatformVersion;
490
491 let platform_version = PlatformVersion::first();
494 let contract = json_document_to_contract(
495 "../rs-drive/tests/supporting_files/contract/family/family-contract.json",
496 false,
497 platform_version,
498 )
499 .expect("expected to load family contract");
500 let doc_type = contract
501 .document_type_for_name("person")
502 .expect("expected person type");
503
504 use crate::data_contract::document_type::random_document::CreateRandomDocument;
506 let document = doc_type
507 .random_document(Some(7), platform_version)
508 .expect("random document");
509 let doc_v0 = match &document {
510 crate::document::Document::V0(d) => d.clone(),
511 };
512
513 let h1 = doc_v0
515 .hash_v0(&contract, doc_type, platform_version)
516 .expect("hash succeeds");
517 let h2 = doc_v0
518 .hash_v0(&contract, doc_type, platform_version)
519 .expect("hash succeeds");
520 assert_eq!(h1, h2);
521 assert_eq!(h1.len(), 32);
523
524 let serialized = doc_v0
527 .serialize(doc_type, &contract, platform_version)
528 .expect("serialize");
529 assert_ne!(h1, serialized);
530 }
531
532 #[test]
533 fn hash_v0_differs_between_different_documents() {
534 use crate::document::document_methods::DocumentHashV0Method;
535 use crate::tests::json_document::json_document_to_contract;
536 use platform_version::version::PlatformVersion;
537
538 let platform_version = PlatformVersion::first();
539 let contract = json_document_to_contract(
540 "../rs-drive/tests/supporting_files/contract/family/family-contract.json",
541 false,
542 platform_version,
543 )
544 .expect("family contract");
545 let doc_type = contract
546 .document_type_for_name("person")
547 .expect("person type");
548
549 use crate::data_contract::document_type::random_document::CreateRandomDocument;
550 let crate::document::Document::V0(doc_a) = doc_type
551 .random_document(Some(1), platform_version)
552 .expect("random a");
553 let crate::document::Document::V0(doc_b) = doc_type
554 .random_document(Some(2), platform_version)
555 .expect("random b");
556
557 let h_a = doc_a
558 .hash_v0(&contract, doc_type, platform_version)
559 .expect("hash a");
560 let h_b = doc_b
561 .hash_v0(&contract, doc_type, platform_version)
562 .expect("hash b");
563 assert_ne!(h_a, h_b);
564 }
565
566 #[test]
572 fn not_equal_when_revision_differs() {
573 let a = minimal_doc();
574 let mut b = minimal_doc();
575 b.revision = Some(1);
576 assert_ne!(a, b);
577 }
578
579 #[test]
580 fn not_equal_when_each_timestamp_differs() {
581 let a = minimal_doc();
582
583 let mut b = minimal_doc();
584 b.created_at = Some(1);
585 assert_ne!(a, b);
586
587 let mut b = minimal_doc();
588 b.updated_at = Some(2);
589 assert_ne!(a, b);
590
591 let mut b = minimal_doc();
592 b.transferred_at = Some(3);
593 assert_ne!(a, b);
594
595 let mut b = minimal_doc();
596 b.created_at_block_height = Some(4);
597 assert_ne!(a, b);
598
599 let mut b = minimal_doc();
600 b.updated_at_block_height = Some(5);
601 assert_ne!(a, b);
602
603 let mut b = minimal_doc();
604 b.transferred_at_block_height = Some(6);
605 assert_ne!(a, b);
606
607 let mut b = minimal_doc();
608 b.created_at_core_block_height = Some(7);
609 assert_ne!(a, b);
610
611 let mut b = minimal_doc();
612 b.updated_at_core_block_height = Some(8);
613 assert_ne!(a, b);
614
615 let mut b = minimal_doc();
616 b.transferred_at_core_block_height = Some(9);
617 assert_ne!(a, b);
618 }
619
620 #[test]
621 fn not_equal_when_properties_differ() {
622 let a = minimal_doc();
623 let mut b = minimal_doc();
624 b.properties.insert("foo".to_string(), Value::U64(1));
625 assert_ne!(a, b);
626 }
627
628 #[test]
629 fn not_equal_when_id_differs() {
630 let a = minimal_doc();
631 let mut b = minimal_doc();
632 b.id = Identifier::new([99u8; 32]);
633 assert_ne!(a, b);
634 }
635
636 #[test]
637 fn not_equal_when_owner_id_differs() {
638 let a = minimal_doc();
639 let mut b = minimal_doc();
640 b.owner_id = Identifier::new([98u8; 32]);
641 assert_ne!(a, b);
642 }
643
644 #[test]
650 fn bump_revision_from_zero_increments_to_one() {
651 let mut doc = minimal_doc();
652 doc.set_revision(Some(0));
653 doc.bump_revision();
654 assert_eq!(doc.revision(), Some(1));
655 }
656
657 #[test]
658 fn bump_revision_from_max_minus_one_reaches_max_then_saturates() {
659 let mut doc = minimal_doc();
660 doc.set_revision(Some(Revision::MAX - 1));
661 doc.bump_revision();
662 assert_eq!(doc.revision(), Some(Revision::MAX));
663 doc.bump_revision();
664 assert_eq!(doc.revision(), Some(Revision::MAX));
665 doc.bump_revision();
667 assert_eq!(doc.revision(), Some(Revision::MAX));
668 }
669
670 #[test]
676 fn setters_round_trip_every_field() {
677 use crate::document::{DocumentV0Getters, DocumentV0Setters};
678 let mut doc = DocumentV0::default();
679 doc.set_id(Identifier::new([1u8; 32]));
680 doc.set_owner_id(Identifier::new([2u8; 32]));
681 let mut props = BTreeMap::new();
682 props.insert("a".to_string(), Value::U64(99));
683 doc.set_properties(props.clone());
684 doc.set_revision(Some(4));
685 doc.set_created_at(Some(10));
686 doc.set_updated_at(Some(20));
687 doc.set_transferred_at(Some(30));
688 doc.set_created_at_block_height(Some(100));
689 doc.set_updated_at_block_height(Some(200));
690 doc.set_transferred_at_block_height(Some(300));
691 doc.set_created_at_core_block_height(Some(1));
692 doc.set_updated_at_core_block_height(Some(2));
693 doc.set_transferred_at_core_block_height(Some(3));
694 doc.set_creator_id(Some(Identifier::new([9u8; 32])));
695
696 assert_eq!(doc.id(), Identifier::new([1u8; 32]));
697 assert_eq!(doc.owner_id(), Identifier::new([2u8; 32]));
698 assert_eq!(doc.properties(), &props);
699 assert_eq!(doc.revision(), Some(4));
700 assert_eq!(doc.created_at(), Some(10));
701 assert_eq!(doc.updated_at(), Some(20));
702 assert_eq!(doc.transferred_at(), Some(30));
703 assert_eq!(doc.created_at_block_height(), Some(100));
704 assert_eq!(doc.updated_at_block_height(), Some(200));
705 assert_eq!(doc.transferred_at_block_height(), Some(300));
706 assert_eq!(doc.created_at_core_block_height(), Some(1));
707 assert_eq!(doc.updated_at_core_block_height(), Some(2));
708 assert_eq!(doc.transferred_at_core_block_height(), Some(3));
709 assert_eq!(doc.creator_id(), Some(Identifier::new([9u8; 32])));
710
711 assert_eq!(doc.id_ref(), &Identifier::new([1u8; 32]));
714 assert_eq!(doc.owner_id_ref(), &Identifier::new([2u8; 32]));
715 assert_eq!(doc.clone().properties_consumed(), props);
716 }
717
718 #[test]
724 fn properties_mut_allows_inserting_new_key() {
725 use crate::document::DocumentV0Getters;
726 let mut doc = minimal_doc();
727 doc.properties_mut().insert("k".into(), Value::U64(7));
728 assert_eq!(doc.properties().get("k"), Some(&Value::U64(7)));
729 }
730
731 #[test]
738 fn debug_format_contains_field_names() {
739 let doc = minimal_doc();
740 let dbg = format!("{:?}", doc);
741 assert!(dbg.contains("DocumentV0"), "expected struct name in Debug");
742 assert!(dbg.contains("id"));
743 assert!(dbg.contains("owner_id"));
744 }
745
746 #[test]
753 fn display_with_only_creator_id_and_no_timestamps() {
754 let mut doc = minimal_doc();
755 doc.creator_id = Some(Identifier::new([7u8; 32]));
756 let s = format!("{}", doc);
757 assert!(s.contains("creator_id:"));
758 assert!(!s.contains("created_at:"));
760 assert!(!s.contains("updated_at:"));
761 assert!(!s.contains("transferred_at:"));
762 assert!(s.contains("no properties"));
764 }
765}