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(
107 feature = "serde-conversion",
108 serde(
109 rename = "$contractVersion",
110 default,
111 skip_serializing_if = "Option::is_none"
112 )
113 )]
114 pub contract_version: Option<u32>,
115}
116
117impl DocumentGetRawForContractV0 for DocumentV0 {
118 }
120
121impl DocumentIsEqualIgnoringTimestampsV0 for DocumentV0 {
122 }
124
125impl DocumentGetRawForDocumentTypeV0 for DocumentV0 {
126 }
128
129impl DocumentHashV0Method for DocumentV0 {
130 }
132
133impl fmt::Display for DocumentV0 {
134 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
135 write!(f, "id:{} ", self.id)?;
136 write!(f, "owner_id:{} ", self.owner_id)?;
137 if let Some(created_at) = self.created_at {
138 let datetime = DateTime::from_timestamp_millis(created_at as i64).unwrap_or_default();
139 write!(f, "created_at:{} ", datetime.format("%Y-%m-%d %H:%M:%S"))?;
140 }
141 if let Some(updated_at) = self.updated_at {
142 let datetime = DateTime::from_timestamp_millis(updated_at as i64).unwrap_or_default();
143 write!(f, "updated_at:{} ", datetime.format("%Y-%m-%d %H:%M:%S"))?;
144 }
145 if let Some(transferred_at) = self.transferred_at {
146 let datetime =
147 DateTime::from_timestamp_millis(transferred_at as i64).unwrap_or_default();
148 write!(
149 f,
150 "transferred_at:{} ",
151 datetime.format("%Y-%m-%d %H:%M:%S")
152 )?;
153 }
154
155 if let Some(created_at_block_height) = self.created_at_block_height {
156 write!(f, "created_at_block_height:{} ", created_at_block_height)?;
157 }
158 if let Some(updated_at_block_height) = self.updated_at_block_height {
159 write!(f, "updated_at_block_height:{} ", updated_at_block_height)?;
160 }
161 if let Some(transferred_at_block_height) = self.transferred_at_block_height {
162 write!(
163 f,
164 "transferred_at_block_height:{} ",
165 transferred_at_block_height
166 )?;
167 }
168 if let Some(created_at_core_block_height) = self.created_at_core_block_height {
169 write!(
170 f,
171 "created_at_core_block_height:{} ",
172 created_at_core_block_height
173 )?;
174 }
175 if let Some(updated_at_core_block_height) = self.updated_at_core_block_height {
176 write!(
177 f,
178 "updated_at_core_block_height:{} ",
179 updated_at_core_block_height
180 )?;
181 }
182 if let Some(transferred_at_core_block_height) = self.transferred_at_core_block_height {
183 write!(
184 f,
185 "transferred_at_core_block_height:{} ",
186 transferred_at_core_block_height
187 )?;
188 }
189
190 if let Some(creator_id) = self.creator_id {
191 write!(f, "creator_id:{} ", creator_id)?;
192 }
193
194 if self.properties.is_empty() {
195 write!(f, "no properties")?;
196 } else {
197 for (key, value) in self.properties.iter() {
198 write!(f, "{}:{} ", key, value)?
199 }
200 }
201 Ok(())
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::data_contract::accessors::v0::DataContractV0Getters;
209 use crate::document::{DocumentV0Getters, DocumentV0Setters};
210 use platform_value::Identifier;
211
212 fn minimal_doc() -> DocumentV0 {
213 DocumentV0 {
214 contract_version: None,
215 id: Identifier::new([1u8; 32]),
216 owner_id: Identifier::new([2u8; 32]),
217 properties: BTreeMap::new(),
218 revision: None,
219 created_at: None,
220 updated_at: None,
221 transferred_at: None,
222 created_at_block_height: None,
223 updated_at_block_height: None,
224 transferred_at_block_height: None,
225 created_at_core_block_height: None,
226 updated_at_core_block_height: None,
227 transferred_at_core_block_height: None,
228 creator_id: None,
229 }
230 }
231
232 #[test]
237 fn display_minimal_document_has_no_properties_marker() {
238 let doc = minimal_doc();
239 let s = format!("{}", doc);
240 assert!(s.contains("id:"), "should contain id");
241 assert!(s.contains("owner_id:"), "should contain owner_id");
242 assert!(
243 s.contains("no properties"),
244 "empty properties should render as 'no properties', got: {s}"
245 );
246 }
247
248 #[test]
249 fn display_with_properties_formats_key_value_pairs() {
250 let mut doc = minimal_doc();
251 doc.properties
252 .insert("name".to_string(), Value::Text("Bob".to_string()));
253 let s = format!("{}", doc);
254 assert!(!s.contains("no properties"));
255 assert!(s.contains("name:"), "should contain property key");
256 }
257
258 #[test]
259 fn display_formats_all_optional_timestamp_fields() {
260 let mut doc = minimal_doc();
261 doc.created_at = Some(1_700_000_000_000);
263 doc.updated_at = Some(1_700_000_100_000);
264 doc.transferred_at = Some(1_700_000_200_000);
265 doc.created_at_block_height = Some(10);
266 doc.updated_at_block_height = Some(20);
267 doc.transferred_at_block_height = Some(30);
268 doc.created_at_core_block_height = Some(1);
269 doc.updated_at_core_block_height = Some(2);
270 doc.transferred_at_core_block_height = Some(3);
271 doc.creator_id = Some(Identifier::new([9u8; 32]));
272
273 let s = format!("{}", doc);
274 assert!(s.contains("created_at:"), "missing created_at: {s}");
276 assert!(s.contains("updated_at:"), "missing updated_at: {s}");
277 assert!(s.contains("transferred_at:"), "missing transferred_at: {s}");
278 assert!(
279 s.contains("created_at_block_height:10"),
280 "missing created_at_block_height: {s}"
281 );
282 assert!(
283 s.contains("updated_at_block_height:20"),
284 "missing updated_at_block_height: {s}"
285 );
286 assert!(
287 s.contains("transferred_at_block_height:30"),
288 "missing transferred_at_block_height: {s}"
289 );
290 assert!(
291 s.contains("created_at_core_block_height:1"),
292 "missing created_at_core_block_height: {s}"
293 );
294 assert!(
295 s.contains("updated_at_core_block_height:2"),
296 "missing updated_at_core_block_height: {s}"
297 );
298 assert!(
299 s.contains("transferred_at_core_block_height:3"),
300 "missing transferred_at_core_block_height: {s}"
301 );
302 assert!(s.contains("creator_id:"), "missing creator_id: {s}");
303 }
304
305 #[test]
306 fn display_invalid_timestamp_uses_default_formatter() {
307 let mut doc = minimal_doc();
310 doc.created_at = Some(i64::MAX as u64);
315 let s = format!("{}", doc);
316 assert!(s.contains("created_at:"));
318 }
319
320 #[test]
325 fn bump_revision_increments_when_some() {
326 let mut doc = minimal_doc();
327 doc.set_revision(Some(5));
328 doc.bump_revision();
329 assert_eq!(doc.revision(), Some(6));
330 }
331
332 #[test]
333 fn bump_revision_is_noop_when_none() {
334 let mut doc = minimal_doc();
335 assert_eq!(doc.revision(), None);
336 doc.bump_revision();
337 assert_eq!(doc.revision(), None);
339 }
340
341 #[test]
342 fn bump_revision_saturates_at_max() {
343 let mut doc = minimal_doc();
344 doc.set_revision(Some(Revision::MAX));
345 doc.bump_revision();
346 assert_eq!(doc.revision(), Some(Revision::MAX));
348 }
349
350 #[test]
355 fn default_document_has_zero_identifiers_and_none_fields() {
356 let doc = DocumentV0::default();
357 assert_eq!(doc.id, Identifier::new([0u8; 32]));
358 assert_eq!(doc.owner_id, Identifier::new([0u8; 32]));
359 assert!(doc.properties.is_empty());
360 assert_eq!(doc.revision, None);
361 assert_eq!(doc.created_at, None);
362 assert_eq!(doc.updated_at, None);
363 assert_eq!(doc.transferred_at, None);
364 assert_eq!(doc.creator_id, None);
365 }
366
367 #[test]
372 fn documents_with_different_creator_id_are_not_equal() {
373 let a = minimal_doc();
374 let mut b = minimal_doc();
375 b.creator_id = Some(Identifier::new([7u8; 32]));
376 assert_ne!(a, b);
377 }
378
379 #[test]
380 fn documents_with_equal_fields_are_equal() {
381 let a = minimal_doc();
382 let b = minimal_doc();
383 assert_eq!(a, b);
384 }
385
386 #[test]
387 fn clone_produces_equal_document() {
388 let mut doc = minimal_doc();
389 doc.properties.insert("k".to_string(), Value::U64(42));
390 doc.revision = Some(3);
391 let cloned = doc.clone();
392 assert_eq!(doc, cloned);
393 }
394
395 #[test]
400 fn display_writes_properties_in_btreemap_sorted_order() {
401 let mut doc = minimal_doc();
406 doc.properties
407 .insert("zebra".to_string(), Value::Text("z".into()));
408 doc.properties
409 .insert("apple".to_string(), Value::Text("a".into()));
410 doc.properties
411 .insert("mango".to_string(), Value::Text("m".into()));
412
413 let s = format!("{}", doc);
414 let apple_idx = s.find("apple:").expect("apple missing");
415 let mango_idx = s.find("mango:").expect("mango missing");
416 let zebra_idx = s.find("zebra:").expect("zebra missing");
417 assert!(
418 apple_idx < mango_idx && mango_idx < zebra_idx,
419 "properties should appear in sorted (BTreeMap) order: {s}"
420 );
421 }
422
423 #[test]
424 fn display_mixes_system_fields_and_user_properties() {
425 let mut doc = minimal_doc();
430 doc.revision = Some(42);
431 doc.created_at_block_height = Some(7);
432 doc.properties
433 .insert("greeting".to_string(), Value::Text("hi".into()));
434
435 let s = format!("{}", doc);
436 assert!(s.contains("created_at_block_height:7"));
437 assert!(s.contains("greeting:"));
438 assert!(!s.contains("revision"));
441 }
442
443 #[test]
450 fn hash_v0_produces_deterministic_output_for_identical_documents() {
451 use crate::document::document_methods::DocumentHashV0Method;
452 use crate::document::serialization_traits::DocumentPlatformConversionMethodsV0;
453 use crate::tests::json_document::json_document_to_contract;
454 use platform_version::version::PlatformVersion;
455
456 let platform_version = PlatformVersion::first();
459 let contract = json_document_to_contract(
460 "../rs-drive/tests/supporting_files/contract/family/family-contract.json",
461 false,
462 platform_version,
463 )
464 .expect("expected to load family contract");
465 let doc_type = contract
466 .document_type_for_name("person")
467 .expect("expected person type");
468
469 use crate::data_contract::document_type::random_document::CreateRandomDocument;
471 let document = doc_type
472 .random_document(Some(7), platform_version)
473 .expect("random document");
474 let doc_v0 = match &document {
475 crate::document::Document::V0(d) => d.clone(),
476 };
477
478 let h1 = doc_v0
480 .hash_v0(&contract, doc_type, platform_version)
481 .expect("hash succeeds");
482 let h2 = doc_v0
483 .hash_v0(&contract, doc_type, platform_version)
484 .expect("hash succeeds");
485 assert_eq!(h1, h2);
486 assert_eq!(h1.len(), 32);
488
489 let serialized = doc_v0
492 .serialize(doc_type, &contract, platform_version)
493 .expect("serialize");
494 assert_ne!(h1, serialized);
495 }
496
497 #[test]
498 fn hash_v0_differs_between_different_documents() {
499 use crate::document::document_methods::DocumentHashV0Method;
500 use crate::tests::json_document::json_document_to_contract;
501 use platform_version::version::PlatformVersion;
502
503 let platform_version = PlatformVersion::first();
504 let contract = json_document_to_contract(
505 "../rs-drive/tests/supporting_files/contract/family/family-contract.json",
506 false,
507 platform_version,
508 )
509 .expect("family contract");
510 let doc_type = contract
511 .document_type_for_name("person")
512 .expect("person type");
513
514 use crate::data_contract::document_type::random_document::CreateRandomDocument;
515 let crate::document::Document::V0(doc_a) = doc_type
516 .random_document(Some(1), platform_version)
517 .expect("random a");
518 let crate::document::Document::V0(doc_b) = doc_type
519 .random_document(Some(2), platform_version)
520 .expect("random b");
521
522 let h_a = doc_a
523 .hash_v0(&contract, doc_type, platform_version)
524 .expect("hash a");
525 let h_b = doc_b
526 .hash_v0(&contract, doc_type, platform_version)
527 .expect("hash b");
528 assert_ne!(h_a, h_b);
529 }
530
531 #[test]
537 fn not_equal_when_revision_differs() {
538 let a = minimal_doc();
539 let mut b = minimal_doc();
540 b.revision = Some(1);
541 assert_ne!(a, b);
542 }
543
544 #[test]
545 fn not_equal_when_each_timestamp_differs() {
546 let a = minimal_doc();
547
548 let mut b = minimal_doc();
549 b.created_at = Some(1);
550 assert_ne!(a, b);
551
552 let mut b = minimal_doc();
553 b.updated_at = Some(2);
554 assert_ne!(a, b);
555
556 let mut b = minimal_doc();
557 b.transferred_at = Some(3);
558 assert_ne!(a, b);
559
560 let mut b = minimal_doc();
561 b.created_at_block_height = Some(4);
562 assert_ne!(a, b);
563
564 let mut b = minimal_doc();
565 b.updated_at_block_height = Some(5);
566 assert_ne!(a, b);
567
568 let mut b = minimal_doc();
569 b.transferred_at_block_height = Some(6);
570 assert_ne!(a, b);
571
572 let mut b = minimal_doc();
573 b.created_at_core_block_height = Some(7);
574 assert_ne!(a, b);
575
576 let mut b = minimal_doc();
577 b.updated_at_core_block_height = Some(8);
578 assert_ne!(a, b);
579
580 let mut b = minimal_doc();
581 b.transferred_at_core_block_height = Some(9);
582 assert_ne!(a, b);
583 }
584
585 #[test]
586 fn not_equal_when_properties_differ() {
587 let a = minimal_doc();
588 let mut b = minimal_doc();
589 b.properties.insert("foo".to_string(), Value::U64(1));
590 assert_ne!(a, b);
591 }
592
593 #[test]
594 fn not_equal_when_id_differs() {
595 let a = minimal_doc();
596 let mut b = minimal_doc();
597 b.id = Identifier::new([99u8; 32]);
598 assert_ne!(a, b);
599 }
600
601 #[test]
602 fn not_equal_when_owner_id_differs() {
603 let a = minimal_doc();
604 let mut b = minimal_doc();
605 b.owner_id = Identifier::new([98u8; 32]);
606 assert_ne!(a, b);
607 }
608
609 #[test]
615 fn bump_revision_from_zero_increments_to_one() {
616 let mut doc = minimal_doc();
617 doc.set_revision(Some(0));
618 doc.bump_revision();
619 assert_eq!(doc.revision(), Some(1));
620 }
621
622 #[test]
623 fn bump_revision_from_max_minus_one_reaches_max_then_saturates() {
624 let mut doc = minimal_doc();
625 doc.set_revision(Some(Revision::MAX - 1));
626 doc.bump_revision();
627 assert_eq!(doc.revision(), Some(Revision::MAX));
628 doc.bump_revision();
629 assert_eq!(doc.revision(), Some(Revision::MAX));
630 doc.bump_revision();
632 assert_eq!(doc.revision(), Some(Revision::MAX));
633 }
634
635 #[test]
641 fn setters_round_trip_every_field() {
642 use crate::document::{DocumentV0Getters, DocumentV0Setters};
643 let mut doc = DocumentV0::default();
644 doc.set_id(Identifier::new([1u8; 32]));
645 doc.set_owner_id(Identifier::new([2u8; 32]));
646 let mut props = BTreeMap::new();
647 props.insert("a".to_string(), Value::U64(99));
648 doc.set_properties(props.clone());
649 doc.set_revision(Some(4));
650 doc.set_created_at(Some(10));
651 doc.set_updated_at(Some(20));
652 doc.set_transferred_at(Some(30));
653 doc.set_created_at_block_height(Some(100));
654 doc.set_updated_at_block_height(Some(200));
655 doc.set_transferred_at_block_height(Some(300));
656 doc.set_created_at_core_block_height(Some(1));
657 doc.set_updated_at_core_block_height(Some(2));
658 doc.set_transferred_at_core_block_height(Some(3));
659 doc.set_creator_id(Some(Identifier::new([9u8; 32])));
660
661 assert_eq!(doc.id(), Identifier::new([1u8; 32]));
662 assert_eq!(doc.owner_id(), Identifier::new([2u8; 32]));
663 assert_eq!(doc.properties(), &props);
664 assert_eq!(doc.revision(), Some(4));
665 assert_eq!(doc.created_at(), Some(10));
666 assert_eq!(doc.updated_at(), Some(20));
667 assert_eq!(doc.transferred_at(), Some(30));
668 assert_eq!(doc.created_at_block_height(), Some(100));
669 assert_eq!(doc.updated_at_block_height(), Some(200));
670 assert_eq!(doc.transferred_at_block_height(), Some(300));
671 assert_eq!(doc.created_at_core_block_height(), Some(1));
672 assert_eq!(doc.updated_at_core_block_height(), Some(2));
673 assert_eq!(doc.transferred_at_core_block_height(), Some(3));
674 assert_eq!(doc.creator_id(), Some(Identifier::new([9u8; 32])));
675
676 assert_eq!(doc.id_ref(), &Identifier::new([1u8; 32]));
679 assert_eq!(doc.owner_id_ref(), &Identifier::new([2u8; 32]));
680 assert_eq!(doc.clone().properties_consumed(), props);
681 }
682
683 #[test]
689 fn properties_mut_allows_inserting_new_key() {
690 use crate::document::DocumentV0Getters;
691 let mut doc = minimal_doc();
692 doc.properties_mut().insert("k".into(), Value::U64(7));
693 assert_eq!(doc.properties().get("k"), Some(&Value::U64(7)));
694 }
695
696 #[test]
703 fn debug_format_contains_field_names() {
704 let doc = minimal_doc();
705 let dbg = format!("{:?}", doc);
706 assert!(dbg.contains("DocumentV0"), "expected struct name in Debug");
707 assert!(dbg.contains("id"));
708 assert!(dbg.contains("owner_id"));
709 }
710
711 #[test]
718 fn display_with_only_creator_id_and_no_timestamps() {
719 let mut doc = minimal_doc();
720 doc.creator_id = Some(Identifier::new([7u8; 32]));
721 let s = format!("{}", doc);
722 assert!(s.contains("creator_id:"));
723 assert!(!s.contains("created_at:"));
725 assert!(!s.contains("updated_at:"));
726 assert!(!s.contains("transferred_at:"));
727 assert!(s.contains("no properties"));
729 }
730}