Skip to main content

dpp/document/
mod.rs

1pub use fields::{property_names, IDENTIFIER_FIELDS};
2
3mod accessors;
4pub mod document_event;
5#[cfg(feature = "client")]
6mod document_facade;
7#[cfg(feature = "factories")]
8pub mod document_factory;
9pub mod document_methods;
10mod document_patch;
11pub mod errors;
12#[cfg(feature = "extended-document")]
13pub mod extended_document;
14mod fields;
15pub mod generate_document_id;
16pub mod serialization_traits;
17#[cfg(feature = "factories")]
18pub mod specialized_document_factory;
19pub mod transfer;
20mod v0;
21
22pub use accessors::*;
23pub use v0::*;
24
25#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
26use crate::serialization::JsonConvertible;
27#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
28use crate::serialization::ValueConvertible;
29#[cfg(feature = "extended-document")]
30pub use extended_document::property_names as extended_document_property_names;
31#[cfg(feature = "extended-document")]
32pub use extended_document::ExtendedDocument;
33#[cfg(feature = "extended-document")]
34pub use extended_document::IDENTIFIER_FIELDS as EXTENDED_DOCUMENT_IDENTIFIER_FIELDS;
35
36/// the initial revision of newly created document
37pub const INITIAL_REVISION: u64 = 1;
38
39use crate::data_contract::document_type::DocumentTypeRef;
40use crate::data_contract::DataContract;
41use crate::document::document_methods::{
42    DocumentGetRawArrayElementsForDocumentTypeV0, DocumentGetRawForContractV0,
43    DocumentGetRawForDocumentTypeV0, DocumentHashV0Method, DocumentIsEqualIgnoringTimestampsV0,
44    DocumentMethodsV0,
45};
46use crate::document::errors::DocumentError;
47use crate::version::PlatformVersion;
48use crate::ProtocolError;
49use derive_more::From;
50
51use std::fmt;
52use std::fmt::Formatter;
53
54#[derive(Clone, Debug, PartialEq, From)]
55#[cfg_attr(
56    feature = "serde-conversion",
57    derive(serde::Serialize, serde::Deserialize),
58    serde(tag = "$formatVersion")
59)]
60pub enum Document {
61    #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))]
62    V0(DocumentV0),
63}
64
65#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
66impl JsonConvertible for Document {}
67
68#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
69impl ValueConvertible for Document {}
70
71impl fmt::Display for Document {
72    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
73        match self {
74            Document::V0(v0) => {
75                write!(f, "v0 : {} ", v0)?;
76            }
77        }
78        Ok(())
79    }
80}
81
82impl DocumentMethodsV0 for Document {
83    /// Return a value given the path to its key and the document type for a contract.
84    fn get_raw_for_contract(
85        &self,
86        key: &str,
87        document_type_name: &str,
88        contract: &DataContract,
89        owner_id: Option<[u8; 32]>,
90        platform_version: &PlatformVersion,
91    ) -> Result<Option<Vec<u8>>, ProtocolError> {
92        match self {
93            Document::V0(document_v0) => {
94                match platform_version
95                    .dpp
96                    .document_versions
97                    .document_method_versions
98                    .get_raw_for_contract
99                {
100                    0 => document_v0.get_raw_for_contract_v0(
101                        key,
102                        document_type_name,
103                        contract,
104                        owner_id,
105                        platform_version,
106                    ),
107                    version => Err(ProtocolError::UnknownVersionMismatch {
108                        method: "DocumentMethodV0::get_raw_for_contract".to_string(),
109                        known_versions: vec![0],
110                        received: version,
111                    }),
112                }
113            }
114        }
115    }
116
117    /// Return a value given the path to its key for a document type.
118    fn get_raw_for_document_type(
119        &self,
120        key_path: &str,
121        document_type: DocumentTypeRef,
122        owner_id: Option<[u8; 32]>,
123        platform_version: &PlatformVersion,
124    ) -> Result<Option<Vec<u8>>, ProtocolError> {
125        match self {
126            Document::V0(document_v0) => {
127                match platform_version
128                    .dpp
129                    .document_versions
130                    .document_method_versions
131                    .get_raw_for_document_type
132                {
133                    0 => document_v0.get_raw_for_document_type_v0(
134                        key_path,
135                        document_type,
136                        owner_id,
137                        platform_version,
138                    ),
139                    version => Err(ProtocolError::UnknownVersionMismatch {
140                        method: "DocumentMethodV0::get_raw_for_document_type".to_string(),
141                        known_versions: vec![0],
142                        received: version,
143                    }),
144                }
145            }
146        }
147    }
148
149    fn get_raw_array_elements_for_document_type(
150        &self,
151        key_path: &str,
152        document_type: DocumentTypeRef,
153        platform_version: &PlatformVersion,
154    ) -> Result<Vec<Vec<u8>>, ProtocolError> {
155        match self {
156            Document::V0(document_v0) => {
157                match platform_version
158                    .dpp
159                    .document_versions
160                    .document_method_versions
161                    .get_raw_array_elements_for_document_type
162                {
163                    Some(0) => document_v0.get_raw_array_elements_for_document_type_v0(
164                        key_path,
165                        document_type,
166                        platform_version,
167                    ),
168                    Some(version) => Err(ProtocolError::UnknownVersionMismatch {
169                        method: "DocumentMethodV0::get_raw_array_elements_for_document_type"
170                            .to_string(),
171                        known_versions: vec![0],
172                        received: version,
173                    }),
174                    None => Err(ProtocolError::UnknownVersionError(
175                        "DocumentMethodV0::get_raw_array_elements_for_document_type is not \
176                         active before protocol version 14"
177                            .to_string(),
178                    )),
179                }
180            }
181        }
182    }
183
184    fn hash(
185        &self,
186        contract: &DataContract,
187        document_type: DocumentTypeRef,
188        platform_version: &PlatformVersion,
189    ) -> Result<Vec<u8>, ProtocolError> {
190        match self {
191            Document::V0(document_v0) => {
192                match platform_version
193                    .dpp
194                    .document_versions
195                    .document_method_versions
196                    .hash
197                {
198                    0 => document_v0.hash_v0(contract, document_type, platform_version),
199                    version => Err(ProtocolError::UnknownVersionMismatch {
200                        method: "DocumentMethodV0::hash".to_string(),
201                        known_versions: vec![0],
202                        received: version,
203                    }),
204                }
205            }
206        }
207    }
208
209    fn increment_revision(&mut self) -> Result<(), ProtocolError> {
210        let Some(revision) = self.revision() else {
211            return Err(ProtocolError::Document(Box::new(
212                DocumentError::DocumentNoRevisionError {
213                    document: Box::new(self.clone()),
214                },
215            )));
216        };
217
218        let new_revision = revision
219            .checked_add(1)
220            .ok_or(ProtocolError::Overflow("overflow when adding 1"))?;
221
222        self.set_revision(Some(new_revision));
223
224        Ok(())
225    }
226
227    fn is_equal_ignoring_time_based_fields(
228        &self,
229        rhs: &Self,
230        also_ignore_fields: Option<Vec<&str>>,
231        platform_version: &PlatformVersion,
232    ) -> Result<bool, ProtocolError> {
233        match (self, rhs) {
234            (Document::V0(document_v0), Document::V0(rhs_v0)) => {
235                match platform_version
236                    .dpp
237                    .document_versions
238                    .document_method_versions
239                    .is_equal_ignoring_timestamps
240                {
241                    0 => Ok(document_v0
242                        .is_equal_ignoring_time_based_fields_v0(rhs_v0, also_ignore_fields)),
243                    version => Err(ProtocolError::UnknownVersionMismatch {
244                        method: "DocumentMethodV0::is_equal_ignoring_time_based_fields".to_string(),
245                        known_versions: vec![0],
246                        received: version,
247                    }),
248                }
249            }
250        }
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use crate::data_contract::accessors::v0::DataContractV0Getters;
258    use crate::data_contract::document_type::random_document::CreateRandomDocument;
259    use crate::document::serialization_traits::DocumentPlatformConversionMethodsV0;
260    use crate::tests::json_document::json_document_to_contract;
261
262    use regex::Regex;
263
264    #[test]
265    fn test_document_display() {
266        let platform_version = PlatformVersion::first();
267        let contract = json_document_to_contract(
268            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
269            false,
270            platform_version,
271        )
272        .expect("expected to get contract");
273
274        let document_type = contract
275            .document_type_for_name("profile")
276            .expect("expected to get profile document type");
277        let document = document_type
278            .random_document(Some(3333), platform_version)
279            .expect("expected to get a random document");
280
281        let document_string = format!("{}", document);
282        let pattern = r"v\d+ : id:45ZNwGcxeMpLpYmiVEKKBKXbZfinrhjZLkau1GWizPFX owner_id:2vq574DjKi7ZD8kJ6dMHxT5wu6ZKD2bW5xKAyKAGW7qZ created_at:(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) updated_at:(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) avatarUrl:string y8RD1DbW18RuyblDX7hx\[...\(670\)\] displayName:string y94Itl6mn1yBE publicMessage:string SvAQrzsslj0ESc15GQBQ\[...\(105\)\] .*";
283        let re = Regex::new(pattern).unwrap();
284        assert!(
285            re.is_match(document_string.as_str()),
286            "pattern: {} does not match {}",
287            pattern,
288            document_string
289        );
290    }
291
292    #[test]
293    fn test_serialization_and_deserialization() {
294        let platform_version = PlatformVersion::latest();
295        let contract = json_document_to_contract(
296            "../rs-drive/tests/supporting_files/contract/dpns/dpns-contract.json",
297            false,
298            platform_version,
299        )
300        .expect("expected to get contract");
301
302        let document_type = contract
303            .document_type_for_name("domain")
304            .expect("expected to get document type");
305        for _ in 0..20 {
306            let document = document_type
307                .random_document(None, platform_version)
308                .expect("expected a document");
309            let serialized = <Document as DocumentPlatformConversionMethodsV0>::serialize(
310                &document,
311                document_type,
312                &contract,
313                platform_version,
314            )
315            .expect("should serialize");
316            let _deserialized = Document::from_bytes(&serialized, document_type, platform_version)
317                .expect("expected to deserialize domain document");
318        }
319    }
320
321    #[test]
322    fn test_serialize_deserialize_over_different_versions_of_document_type() {
323        let platform_version = PlatformVersion::latest();
324        let contract = json_document_to_contract(
325            "../rs-drive/tests/supporting_files/contract/dpns/dpns-contract.json",
326            false,
327            platform_version,
328        )
329        .expect("expected to get contract");
330
331        let updated_contract = json_document_to_contract(
332            "../rs-drive/tests/supporting_files/contract/dpns/dpns-contract-update-v2-test.json",
333            false,
334            platform_version,
335        )
336        .expect("expected to get contract");
337
338        let document_type = contract
339            .document_type_for_name("domain")
340            .expect("expected to get document type");
341
342        let updated_document_type = updated_contract
343            .document_type_for_name("domain")
344            .expect("expected to get document type");
345
346        // let's test from a document created in the old version, and we try to deserialize it in the new version
347        for _ in 0..20 {
348            let document = document_type
349                .random_document(None, platform_version)
350                .expect("expected a document");
351            let serialized = <Document as DocumentPlatformConversionMethodsV0>::serialize(
352                &document,
353                document_type,
354                &contract,
355                platform_version,
356            )
357            .expect("should serialize");
358            let _deserialized =
359                Document::from_bytes(&serialized, updated_document_type, platform_version)
360                    .expect("expected to deserialize domain document");
361        }
362
363        // let's test from a document created in the new version, and we try to deserialize it with the old version
364        for _ in 0..20 {
365            let document = updated_document_type
366                .random_document(None, platform_version)
367                .expect("expected a document");
368            let serialized = <Document as DocumentPlatformConversionMethodsV0>::serialize(
369                &document,
370                document_type,
371                &contract,
372                platform_version,
373            )
374            .expect("should serialize");
375            let _deserialized = Document::from_bytes(&serialized, document_type, platform_version)
376                .expect("expected to deserialize domain document");
377        }
378    }
379
380    // ================================================================
381    //  Display impl tests for Document
382    // ================================================================
383
384    #[test]
385    fn display_document_with_no_properties() {
386        let doc = Document::V0(DocumentV0 {
387            contract_version: None,
388            id: platform_value::Identifier::new([0xAA; 32]),
389            owner_id: platform_value::Identifier::new([0xBB; 32]),
390            properties: Default::default(),
391            revision: None,
392            created_at: None,
393            updated_at: None,
394            transferred_at: None,
395            created_at_block_height: None,
396            updated_at_block_height: None,
397            transferred_at_block_height: None,
398            created_at_core_block_height: None,
399            updated_at_core_block_height: None,
400            transferred_at_core_block_height: None,
401            creator_id: None,
402            moderated_at: None,
403            moderated_by: None,
404        });
405
406        let s = format!("{}", doc);
407        assert!(
408            s.contains("no properties"),
409            "should say 'no properties' when the BTreeMap is empty, got: {}",
410            s
411        );
412    }
413
414    #[test]
415    fn display_document_shows_transferred_at_fields() {
416        let doc = Document::V0(DocumentV0 {
417            contract_version: None,
418            id: platform_value::Identifier::new([1u8; 32]),
419            owner_id: platform_value::Identifier::new([2u8; 32]),
420            properties: Default::default(),
421            revision: None,
422            created_at: None,
423            updated_at: None,
424            transferred_at: Some(1_700_000_000_000),
425            created_at_block_height: None,
426            updated_at_block_height: None,
427            transferred_at_block_height: Some(500),
428            created_at_core_block_height: None,
429            updated_at_core_block_height: None,
430            transferred_at_core_block_height: Some(42),
431            creator_id: None,
432            moderated_at: None,
433            moderated_by: None,
434        });
435
436        let s = format!("{}", doc);
437        assert!(
438            s.contains("transferred_at:"),
439            "should contain transferred_at, got: {}",
440            s
441        );
442        assert!(
443            s.contains("transferred_at_block_height:500"),
444            "should contain transferred_at_block_height:500, got: {}",
445            s
446        );
447        assert!(
448            s.contains("transferred_at_core_block_height:42"),
449            "should contain transferred_at_core_block_height:42, got: {}",
450            s
451        );
452    }
453
454    #[test]
455    fn display_document_shows_creator_id() {
456        let creator = platform_value::Identifier::new([0xCC; 32]);
457        let doc = Document::V0(DocumentV0 {
458            contract_version: None,
459            id: platform_value::Identifier::new([1u8; 32]),
460            owner_id: platform_value::Identifier::new([2u8; 32]),
461            properties: Default::default(),
462            revision: None,
463            created_at: None,
464            updated_at: None,
465            transferred_at: None,
466            created_at_block_height: None,
467            updated_at_block_height: None,
468            transferred_at_block_height: None,
469            created_at_core_block_height: None,
470            updated_at_core_block_height: None,
471            transferred_at_core_block_height: None,
472            creator_id: Some(creator),
473            moderated_at: None,
474            moderated_by: None,
475        });
476
477        let s = format!("{}", doc);
478        assert!(
479            s.contains("creator_id:"),
480            "should contain creator_id, got: {}",
481            s
482        );
483    }
484
485    #[test]
486    fn display_document_shows_block_height_fields() {
487        let doc = Document::V0(DocumentV0 {
488            contract_version: None,
489            id: platform_value::Identifier::new([1u8; 32]),
490            owner_id: platform_value::Identifier::new([2u8; 32]),
491            properties: Default::default(),
492            revision: None,
493            created_at: None,
494            updated_at: None,
495            transferred_at: None,
496            created_at_block_height: Some(100),
497            updated_at_block_height: Some(200),
498            transferred_at_block_height: None,
499            created_at_core_block_height: Some(50),
500            updated_at_core_block_height: Some(60),
501            transferred_at_core_block_height: None,
502            creator_id: None,
503            moderated_at: None,
504            moderated_by: None,
505        });
506
507        let s = format!("{}", doc);
508        assert!(s.contains("created_at_block_height:100"), "got: {}", s);
509        assert!(s.contains("updated_at_block_height:200"), "got: {}", s);
510        assert!(s.contains("created_at_core_block_height:50"), "got: {}", s);
511        assert!(s.contains("updated_at_core_block_height:60"), "got: {}", s);
512    }
513
514    // ================================================================
515    //  Version dispatch: increment_revision
516    // ================================================================
517
518    #[test]
519    fn increment_revision_works_on_mutable_document() {
520        let mut doc = Document::V0(DocumentV0 {
521            contract_version: None,
522            id: platform_value::Identifier::new([1u8; 32]),
523            owner_id: platform_value::Identifier::new([2u8; 32]),
524            properties: Default::default(),
525            revision: Some(1),
526            created_at: None,
527            updated_at: None,
528            transferred_at: None,
529            created_at_block_height: None,
530            updated_at_block_height: None,
531            transferred_at_block_height: None,
532            created_at_core_block_height: None,
533            updated_at_core_block_height: None,
534            transferred_at_core_block_height: None,
535            creator_id: None,
536            moderated_at: None,
537            moderated_by: None,
538        });
539
540        doc.increment_revision()
541            .expect("increment_revision should succeed");
542        assert_eq!(doc.revision(), Some(2));
543    }
544
545    #[test]
546    fn increment_revision_fails_when_no_revision() {
547        let mut doc = Document::V0(DocumentV0 {
548            contract_version: None,
549            id: platform_value::Identifier::new([1u8; 32]),
550            owner_id: platform_value::Identifier::new([2u8; 32]),
551            properties: Default::default(),
552            revision: None,
553            created_at: None,
554            updated_at: None,
555            transferred_at: None,
556            created_at_block_height: None,
557            updated_at_block_height: None,
558            transferred_at_block_height: None,
559            created_at_core_block_height: None,
560            updated_at_core_block_height: None,
561            transferred_at_core_block_height: None,
562            creator_id: None,
563            moderated_at: None,
564            moderated_by: None,
565        });
566
567        let result = doc.increment_revision();
568        assert!(
569            result.is_err(),
570            "increment_revision should fail when revision is None"
571        );
572    }
573
574    // ================================================================
575    //  Version dispatch: is_equal_ignoring_time_based_fields
576    // ================================================================
577
578    #[test]
579    fn is_equal_ignoring_time_based_fields_dispatches_correctly() {
580        let platform_version = PlatformVersion::latest();
581        let contract = json_document_to_contract(
582            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
583            false,
584            platform_version,
585        )
586        .expect("expected to get contract");
587
588        let document_type = contract
589            .document_type_for_name("profile")
590            .expect("expected to get profile document type");
591
592        let doc1 = document_type
593            .random_document(Some(42), platform_version)
594            .expect("expected random document");
595
596        let mut doc2 = doc1.clone();
597        // Change timestamps
598        doc2.set_created_at(Some(9_999_999));
599        doc2.set_updated_at(Some(8_888_888));
600
601        let result = doc1
602            .is_equal_ignoring_time_based_fields(&doc2, None, platform_version)
603            .expect("should succeed");
604        assert!(
605            result,
606            "same document with different timestamps should be equal ignoring time fields"
607        );
608    }
609
610    // ================================================================
611    //  Version dispatch: get_raw_for_contract
612    // ================================================================
613
614    #[test]
615    fn get_raw_for_contract_dispatches_to_v0() {
616        let platform_version = PlatformVersion::latest();
617        let contract = json_document_to_contract(
618            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
619            false,
620            platform_version,
621        )
622        .expect("expected to get contract");
623
624        let document_type = contract
625            .document_type_for_name("profile")
626            .expect("expected to get profile document type");
627
628        let document = document_type
629            .random_document(Some(7), platform_version)
630            .expect("expected random document");
631
632        let raw_id = document
633            .get_raw_for_contract("$id", "profile", &contract, None, platform_version)
634            .expect("should succeed");
635        assert_eq!(raw_id, Some(document.id().to_vec()));
636    }
637
638    // ================================================================
639    //  Version dispatch: hash
640    // ================================================================
641
642    #[test]
643    fn document_hash_is_deterministic() {
644        let platform_version = PlatformVersion::latest();
645        let contract = json_document_to_contract(
646            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
647            false,
648            platform_version,
649        )
650        .expect("expected to get contract");
651
652        let document_type = contract
653            .document_type_for_name("profile")
654            .expect("expected to get profile document type");
655
656        let document = document_type
657            .random_document(Some(42), platform_version)
658            .expect("expected random document");
659
660        let hash1 = document
661            .hash(&contract, document_type, platform_version)
662            .expect("hash should succeed");
663        let hash2 = document
664            .hash(&contract, document_type, platform_version)
665            .expect("hash should succeed");
666        assert_eq!(hash1, hash2, "hash should be deterministic");
667        assert!(!hash1.is_empty(), "hash should not be empty");
668    }
669
670    // ================================================================
671    //  increment_revision: overflow from Revision::MAX surfaces an
672    //  Overflow ProtocolError (not a silent saturate — this is the
673    //  Document-enum path which uses checked_add).
674    // ================================================================
675
676    #[test]
677    fn increment_revision_errors_on_overflow() {
678        let mut doc = Document::V0(DocumentV0 {
679            contract_version: None,
680            id: platform_value::Identifier::new([1u8; 32]),
681            owner_id: platform_value::Identifier::new([2u8; 32]),
682            properties: Default::default(),
683            revision: Some(crate::prelude::Revision::MAX),
684            created_at: None,
685            updated_at: None,
686            transferred_at: None,
687            created_at_block_height: None,
688            updated_at_block_height: None,
689            transferred_at_block_height: None,
690            created_at_core_block_height: None,
691            updated_at_core_block_height: None,
692            transferred_at_core_block_height: None,
693            creator_id: None,
694            moderated_at: None,
695            moderated_by: None,
696        });
697        let err = doc.increment_revision().expect_err("MAX + 1 must overflow");
698        match err {
699            ProtocolError::Overflow(_) => {}
700            other => panic!("expected ProtocolError::Overflow, got {:?}", other),
701        }
702    }
703
704    // ================================================================
705    //  From<DocumentV0> for Document produces a V0 variant.
706    // ================================================================
707
708    #[test]
709    fn from_document_v0_produces_v0_variant() {
710        let v0 = DocumentV0 {
711            contract_version: None,
712            id: platform_value::Identifier::new([1u8; 32]),
713            owner_id: platform_value::Identifier::new([2u8; 32]),
714            properties: Default::default(),
715            revision: Some(7),
716            created_at: None,
717            updated_at: None,
718            transferred_at: None,
719            created_at_block_height: None,
720            updated_at_block_height: None,
721            transferred_at_block_height: None,
722            created_at_core_block_height: None,
723            updated_at_core_block_height: None,
724            transferred_at_core_block_height: None,
725            creator_id: None,
726            moderated_at: None,
727            moderated_by: None,
728        };
729        let document: Document = v0.clone().into();
730        match document {
731            Document::V0(inner) => assert_eq!(inner, v0),
732        }
733    }
734
735    // ================================================================
736    //  Document Display forwards to DocumentV0 Display with a version
737    //  prefix.
738    // ================================================================
739
740    #[test]
741    fn document_display_has_version_prefix() {
742        let doc = Document::V0(DocumentV0 {
743            contract_version: None,
744            id: platform_value::Identifier::new([1u8; 32]),
745            owner_id: platform_value::Identifier::new([2u8; 32]),
746            properties: Default::default(),
747            revision: None,
748            created_at: None,
749            updated_at: None,
750            transferred_at: None,
751            created_at_block_height: None,
752            updated_at_block_height: None,
753            transferred_at_block_height: None,
754            created_at_core_block_height: None,
755            updated_at_core_block_height: None,
756            transferred_at_core_block_height: None,
757            creator_id: None,
758            moderated_at: None,
759            moderated_by: None,
760        });
761        let s = format!("{}", doc);
762        assert!(
763            s.starts_with("v0 : "),
764            "Display should prefix with version, got: {s}"
765        );
766    }
767
768    // ================================================================
769    //  get_raw_for_document_type dispatches via platform version 0
770    //  to the V0 implementation.
771    // ================================================================
772
773    #[test]
774    fn get_raw_for_document_type_dispatch_path_returns_id() {
775        let platform_version = PlatformVersion::latest();
776        let contract = json_document_to_contract(
777            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
778            false,
779            platform_version,
780        )
781        .expect("expected contract");
782        let document_type = contract
783            .document_type_for_name("profile")
784            .expect("expected document type");
785
786        let document = document_type
787            .random_document(Some(11), platform_version)
788            .expect("expected random document");
789
790        let raw = document
791            .get_raw_for_document_type("$id", document_type, None, platform_version)
792            .expect("should succeed");
793        assert_eq!(raw, Some(document.id().to_vec()));
794    }
795}
796
797#[cfg(all(
798    test,
799    feature = "json-conversion",
800    feature = "value-conversion",
801    feature = "serde-conversion"
802))]
803mod json_convertible_tests {
804    use super::*;
805
806    use platform_value::{platform_value, Identifier};
807    use serde_json::json;
808    use std::collections::BTreeMap;
809
810    fn fixture() -> Document {
811        Document::V0(DocumentV0 {
812            contract_version: None,
813            id: Identifier::new([0xa1; 32]),
814            owner_id: Identifier::new([0xb2; 32]),
815            properties: BTreeMap::new(),
816            revision: Some(2),
817            created_at: Some(1_700_000_000_000),
818            updated_at: Some(1_700_000_001_000),
819            transferred_at: None,
820            created_at_block_height: Some(100),
821            updated_at_block_height: Some(101),
822            transferred_at_block_height: None,
823            created_at_core_block_height: Some(50),
824            updated_at_core_block_height: Some(51),
825            transferred_at_core_block_height: None,
826            creator_id: Some(Identifier::new([0xc3; 32])),
827            moderated_at: None,
828            moderated_by: None,
829        })
830    }
831
832    #[test]
833    fn json_round_trip_with_full_wire_shape() {
834        use crate::serialization::JsonConvertible;
835        let original = fixture();
836        let json = original.to_json().expect("to_json");
837        // Sized-int fields whose JSON wire encoding loses size info:
838        // `$revision`/`$createdAt`/`$updatedAt`/`$createdAtBlockHeight`/
839        // `$updatedAtBlockHeight` (u64), `$createdAtCoreBlockHeight`/
840        // `$updatedAtCoreBlockHeight` (u32). The value-path locks variants
841        // via explicit suffixes. `properties` is flattened into the document
842        // root; for an empty `BTreeMap`, no extra keys appear.
843        assert_eq!(
844            json,
845            json!({
846                "$formatVersion": "0",
847                "$id": Identifier::new([0xa1; 32]),
848                "$ownerId": Identifier::new([0xb2; 32]),
849                "$revision": 2,
850                "$createdAt": 1_700_000_000_000u64,
851                "$updatedAt": 1_700_000_001_000u64,
852                "$transferredAt": serde_json::Value::Null,
853                "$createdAtBlockHeight": 100,
854                "$updatedAtBlockHeight": 101,
855                "$transferredAtBlockHeight": serde_json::Value::Null,
856                "$createdAtCoreBlockHeight": 50,
857                "$updatedAtCoreBlockHeight": 51,
858                "$transferredAtCoreBlockHeight": serde_json::Value::Null,
859                "$creatorId": Identifier::new([0xc3; 32]),
860            })
861        );
862        let recovered = Document::from_json(json).expect("from_json");
863        assert_eq!(original, recovered);
864    }
865
866    #[test]
867    fn value_round_trip_with_full_wire_shape() {
868        use crate::serialization::ValueConvertible;
869        let original = fixture();
870        let value = original.to_object().expect("to_object");
871        // Explicit suffixes lock in sized variants: revision / *At /
872        // *AtBlockHeight are u64; *AtCoreBlockHeight are u32.
873        assert_eq!(
874            value,
875            platform_value!({
876                "$formatVersion": "0",
877                "$id": Identifier::new([0xa1; 32]),
878                "$ownerId": Identifier::new([0xb2; 32]),
879                "$revision": 2u64,
880                "$createdAt": 1_700_000_000_000u64,
881                "$updatedAt": 1_700_000_001_000u64,
882                "$transferredAt": platform_value::Value::Null,
883                "$createdAtBlockHeight": 100u64,
884                "$updatedAtBlockHeight": 101u64,
885                "$transferredAtBlockHeight": platform_value::Value::Null,
886                "$createdAtCoreBlockHeight": 50u32,
887                "$updatedAtCoreBlockHeight": 51u32,
888                "$transferredAtCoreBlockHeight": platform_value::Value::Null,
889                "$creatorId": Identifier::new([0xc3; 32]),
890            })
891        );
892        let recovered = Document::from_object(value).expect("from_object");
893        assert_eq!(original, recovered);
894    }
895
896    #[test]
897    fn should_carry_the_moderation_stamp_through_json_and_value() {
898        let mut original = fixture();
899        original.set_moderated_at(Some(1_700_000_002_000));
900        original.set_moderated_by(Some(Identifier::new([0xd4; 32])));
901
902        let json = original.to_json().expect("to_json");
903        assert_eq!(json["$moderatedAt"], json!(1_700_000_002_000u64));
904        assert_eq!(json["$moderatedBy"], json!(Identifier::new([0xd4; 32])));
905        assert_eq!(Document::from_json(json).expect("from_json"), original);
906
907        let value = original.to_object().expect("to_object");
908        let map = value.to_btree_ref_string_map().expect("a map");
909        assert_eq!(
910            map.get("$moderatedAt"),
911            Some(&&platform_value::Value::U64(1_700_000_002_000))
912        );
913        assert_eq!(
914            map.get("$moderatedBy"),
915            Some(&&platform_value::Value::Identifier([0xd4; 32]))
916        );
917        assert_eq!(Document::from_object(value).expect("from_object"), original);
918    }
919}