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