1pub use fields::{property_names, IDENTIFIER_FIELDS};
2
3mod accessors;
4#[cfg(feature = "client")]
5mod document_facade;
6#[cfg(feature = "factories")]
7pub mod document_factory;
8pub mod document_methods;
9mod document_patch;
10pub mod errors;
11#[cfg(feature = "extended-document")]
12pub mod extended_document;
13mod fields;
14pub mod generate_document_id;
15pub mod serialization_traits;
16#[cfg(feature = "factories")]
17pub mod specialized_document_factory;
18pub mod transfer;
19mod v0;
20
21pub use accessors::*;
22pub use v0::*;
23
24#[cfg(feature = "extended-document")]
25pub use extended_document::property_names as extended_document_property_names;
26#[cfg(feature = "extended-document")]
27pub use extended_document::ExtendedDocument;
28#[cfg(feature = "extended-document")]
29pub use extended_document::IDENTIFIER_FIELDS as EXTENDED_DOCUMENT_IDENTIFIER_FIELDS;
30
31pub const INITIAL_REVISION: u64 = 1;
33
34use crate::data_contract::document_type::DocumentTypeRef;
35use crate::data_contract::DataContract;
36use crate::document::document_methods::{
37 DocumentGetRawForContractV0, DocumentGetRawForDocumentTypeV0, DocumentHashV0Method,
38 DocumentIsEqualIgnoringTimestampsV0, DocumentMethodsV0,
39};
40use crate::document::errors::DocumentError;
41use crate::version::PlatformVersion;
42use crate::ProtocolError;
43use derive_more::From;
44
45use std::fmt;
46use std::fmt::Formatter;
47
48#[derive(Clone, Debug, PartialEq, From)]
49#[cfg_attr(
50 any(feature = "serde-conversion", feature = "serde-conversion"),
51 derive(serde::Serialize, serde::Deserialize),
52 serde(tag = "$formatVersion")
53)]
54pub enum Document {
55 #[cfg_attr(
56 any(feature = "serde-conversion", feature = "serde-conversion"),
57 serde(rename = "0")
58 )]
59 V0(DocumentV0),
60}
61
62impl fmt::Display for Document {
63 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
64 match self {
65 Document::V0(v0) => {
66 write!(f, "v0 : {} ", v0)?;
67 }
68 }
69 Ok(())
70 }
71}
72
73impl DocumentMethodsV0 for Document {
74 fn get_raw_for_contract(
76 &self,
77 key: &str,
78 document_type_name: &str,
79 contract: &DataContract,
80 owner_id: Option<[u8; 32]>,
81 platform_version: &PlatformVersion,
82 ) -> Result<Option<Vec<u8>>, ProtocolError> {
83 match self {
84 Document::V0(document_v0) => {
85 match platform_version
86 .dpp
87 .document_versions
88 .document_method_versions
89 .get_raw_for_contract
90 {
91 0 => document_v0.get_raw_for_contract_v0(
92 key,
93 document_type_name,
94 contract,
95 owner_id,
96 platform_version,
97 ),
98 version => Err(ProtocolError::UnknownVersionMismatch {
99 method: "DocumentMethodV0::get_raw_for_contract".to_string(),
100 known_versions: vec![0],
101 received: version,
102 }),
103 }
104 }
105 }
106 }
107
108 fn get_raw_for_document_type(
110 &self,
111 key_path: &str,
112 document_type: DocumentTypeRef,
113 owner_id: Option<[u8; 32]>,
114 platform_version: &PlatformVersion,
115 ) -> Result<Option<Vec<u8>>, ProtocolError> {
116 match self {
117 Document::V0(document_v0) => {
118 match platform_version
119 .dpp
120 .document_versions
121 .document_method_versions
122 .get_raw_for_document_type
123 {
124 0 => document_v0.get_raw_for_document_type_v0(
125 key_path,
126 document_type,
127 owner_id,
128 platform_version,
129 ),
130 version => Err(ProtocolError::UnknownVersionMismatch {
131 method: "DocumentMethodV0::get_raw_for_document_type".to_string(),
132 known_versions: vec![0],
133 received: version,
134 }),
135 }
136 }
137 }
138 }
139
140 fn hash(
141 &self,
142 contract: &DataContract,
143 document_type: DocumentTypeRef,
144 platform_version: &PlatformVersion,
145 ) -> Result<Vec<u8>, ProtocolError> {
146 match self {
147 Document::V0(document_v0) => {
148 match platform_version
149 .dpp
150 .document_versions
151 .document_method_versions
152 .hash
153 {
154 0 => document_v0.hash_v0(contract, document_type, platform_version),
155 version => Err(ProtocolError::UnknownVersionMismatch {
156 method: "DocumentMethodV0::hash".to_string(),
157 known_versions: vec![0],
158 received: version,
159 }),
160 }
161 }
162 }
163 }
164
165 fn increment_revision(&mut self) -> Result<(), ProtocolError> {
166 let Some(revision) = self.revision() else {
167 return Err(ProtocolError::Document(Box::new(
168 DocumentError::DocumentNoRevisionError {
169 document: Box::new(self.clone()),
170 },
171 )));
172 };
173
174 let new_revision = revision
175 .checked_add(1)
176 .ok_or(ProtocolError::Overflow("overflow when adding 1"))?;
177
178 self.set_revision(Some(new_revision));
179
180 Ok(())
181 }
182
183 fn is_equal_ignoring_time_based_fields(
184 &self,
185 rhs: &Self,
186 also_ignore_fields: Option<Vec<&str>>,
187 platform_version: &PlatformVersion,
188 ) -> Result<bool, ProtocolError> {
189 match (self, rhs) {
190 (Document::V0(document_v0), Document::V0(rhs_v0)) => {
191 match platform_version
192 .dpp
193 .document_versions
194 .document_method_versions
195 .is_equal_ignoring_timestamps
196 {
197 0 => Ok(document_v0
198 .is_equal_ignoring_time_based_fields_v0(rhs_v0, also_ignore_fields)),
199 version => Err(ProtocolError::UnknownVersionMismatch {
200 method: "DocumentMethodV0::is_equal_ignoring_time_based_fields".to_string(),
201 known_versions: vec![0],
202 received: version,
203 }),
204 }
205 }
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::data_contract::accessors::v0::DataContractV0Getters;
214 use crate::data_contract::document_type::random_document::CreateRandomDocument;
215 use crate::document::serialization_traits::DocumentPlatformConversionMethodsV0;
216 use crate::tests::json_document::json_document_to_contract;
217
218 use regex::Regex;
219
220 #[test]
221 fn test_document_display() {
222 let platform_version = PlatformVersion::first();
223 let contract = json_document_to_contract(
224 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
225 false,
226 platform_version,
227 )
228 .expect("expected to get contract");
229
230 let document_type = contract
231 .document_type_for_name("profile")
232 .expect("expected to get profile document type");
233 let document = document_type
234 .random_document(Some(3333), platform_version)
235 .expect("expected to get a random document");
236
237 let document_string = format!("{}", document);
238 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\)\] .*";
239 let re = Regex::new(pattern).unwrap();
240 assert!(
241 re.is_match(document_string.as_str()),
242 "pattern: {} does not match {}",
243 pattern,
244 document_string
245 );
246 }
247
248 #[test]
249 fn test_serialization_and_deserialization() {
250 let platform_version = PlatformVersion::latest();
251 let contract = json_document_to_contract(
252 "../rs-drive/tests/supporting_files/contract/dpns/dpns-contract.json",
253 false,
254 platform_version,
255 )
256 .expect("expected to get contract");
257
258 let document_type = contract
259 .document_type_for_name("domain")
260 .expect("expected to get document type");
261 for _ in 0..20 {
262 let document = document_type
263 .random_document(None, platform_version)
264 .expect("expected a document");
265 let serialized = <Document as DocumentPlatformConversionMethodsV0>::serialize(
266 &document,
267 document_type,
268 &contract,
269 platform_version,
270 )
271 .expect("should serialize");
272 let _deserialized = Document::from_bytes(&serialized, document_type, platform_version)
273 .expect("expected to deserialize domain document");
274 }
275 }
276
277 #[test]
278 fn test_serialize_deserialize_over_different_versions_of_document_type() {
279 let platform_version = PlatformVersion::latest();
280 let contract = json_document_to_contract(
281 "../rs-drive/tests/supporting_files/contract/dpns/dpns-contract.json",
282 false,
283 platform_version,
284 )
285 .expect("expected to get contract");
286
287 let updated_contract = json_document_to_contract(
288 "../rs-drive/tests/supporting_files/contract/dpns/dpns-contract-update-v2-test.json",
289 false,
290 platform_version,
291 )
292 .expect("expected to get contract");
293
294 let document_type = contract
295 .document_type_for_name("domain")
296 .expect("expected to get document type");
297
298 let updated_document_type = updated_contract
299 .document_type_for_name("domain")
300 .expect("expected to get document type");
301
302 for _ in 0..20 {
304 let document = document_type
305 .random_document(None, platform_version)
306 .expect("expected a document");
307 let serialized = <Document as DocumentPlatformConversionMethodsV0>::serialize(
308 &document,
309 document_type,
310 &contract,
311 platform_version,
312 )
313 .expect("should serialize");
314 let _deserialized =
315 Document::from_bytes(&serialized, updated_document_type, platform_version)
316 .expect("expected to deserialize domain document");
317 }
318
319 for _ in 0..20 {
321 let document = updated_document_type
322 .random_document(None, platform_version)
323 .expect("expected a document");
324 let serialized = <Document as DocumentPlatformConversionMethodsV0>::serialize(
325 &document,
326 document_type,
327 &contract,
328 platform_version,
329 )
330 .expect("should serialize");
331 let _deserialized = Document::from_bytes(&serialized, document_type, platform_version)
332 .expect("expected to deserialize domain document");
333 }
334 }
335
336 #[test]
341 fn display_document_with_no_properties() {
342 let doc = Document::V0(DocumentV0 {
343 id: platform_value::Identifier::new([0xAA; 32]),
344 owner_id: platform_value::Identifier::new([0xBB; 32]),
345 properties: Default::default(),
346 revision: None,
347 created_at: None,
348 updated_at: None,
349 transferred_at: None,
350 created_at_block_height: None,
351 updated_at_block_height: None,
352 transferred_at_block_height: None,
353 created_at_core_block_height: None,
354 updated_at_core_block_height: None,
355 transferred_at_core_block_height: None,
356 creator_id: None,
357 });
358
359 let s = format!("{}", doc);
360 assert!(
361 s.contains("no properties"),
362 "should say 'no properties' when the BTreeMap is empty, got: {}",
363 s
364 );
365 }
366
367 #[test]
368 fn display_document_shows_transferred_at_fields() {
369 let doc = Document::V0(DocumentV0 {
370 id: platform_value::Identifier::new([1u8; 32]),
371 owner_id: platform_value::Identifier::new([2u8; 32]),
372 properties: Default::default(),
373 revision: None,
374 created_at: None,
375 updated_at: None,
376 transferred_at: Some(1_700_000_000_000),
377 created_at_block_height: None,
378 updated_at_block_height: None,
379 transferred_at_block_height: Some(500),
380 created_at_core_block_height: None,
381 updated_at_core_block_height: None,
382 transferred_at_core_block_height: Some(42),
383 creator_id: None,
384 });
385
386 let s = format!("{}", doc);
387 assert!(
388 s.contains("transferred_at:"),
389 "should contain transferred_at, got: {}",
390 s
391 );
392 assert!(
393 s.contains("transferred_at_block_height:500"),
394 "should contain transferred_at_block_height:500, got: {}",
395 s
396 );
397 assert!(
398 s.contains("transferred_at_core_block_height:42"),
399 "should contain transferred_at_core_block_height:42, got: {}",
400 s
401 );
402 }
403
404 #[test]
405 fn display_document_shows_creator_id() {
406 let creator = platform_value::Identifier::new([0xCC; 32]);
407 let doc = Document::V0(DocumentV0 {
408 id: platform_value::Identifier::new([1u8; 32]),
409 owner_id: platform_value::Identifier::new([2u8; 32]),
410 properties: Default::default(),
411 revision: None,
412 created_at: None,
413 updated_at: None,
414 transferred_at: None,
415 created_at_block_height: None,
416 updated_at_block_height: None,
417 transferred_at_block_height: None,
418 created_at_core_block_height: None,
419 updated_at_core_block_height: None,
420 transferred_at_core_block_height: None,
421 creator_id: Some(creator),
422 });
423
424 let s = format!("{}", doc);
425 assert!(
426 s.contains("creator_id:"),
427 "should contain creator_id, got: {}",
428 s
429 );
430 }
431
432 #[test]
433 fn display_document_shows_block_height_fields() {
434 let doc = Document::V0(DocumentV0 {
435 id: platform_value::Identifier::new([1u8; 32]),
436 owner_id: platform_value::Identifier::new([2u8; 32]),
437 properties: Default::default(),
438 revision: None,
439 created_at: None,
440 updated_at: None,
441 transferred_at: None,
442 created_at_block_height: Some(100),
443 updated_at_block_height: Some(200),
444 transferred_at_block_height: None,
445 created_at_core_block_height: Some(50),
446 updated_at_core_block_height: Some(60),
447 transferred_at_core_block_height: None,
448 creator_id: None,
449 });
450
451 let s = format!("{}", doc);
452 assert!(s.contains("created_at_block_height:100"), "got: {}", s);
453 assert!(s.contains("updated_at_block_height:200"), "got: {}", s);
454 assert!(s.contains("created_at_core_block_height:50"), "got: {}", s);
455 assert!(s.contains("updated_at_core_block_height:60"), "got: {}", s);
456 }
457
458 #[test]
463 fn increment_revision_works_on_mutable_document() {
464 let mut doc = Document::V0(DocumentV0 {
465 id: platform_value::Identifier::new([1u8; 32]),
466 owner_id: platform_value::Identifier::new([2u8; 32]),
467 properties: Default::default(),
468 revision: Some(1),
469 created_at: None,
470 updated_at: None,
471 transferred_at: None,
472 created_at_block_height: None,
473 updated_at_block_height: None,
474 transferred_at_block_height: None,
475 created_at_core_block_height: None,
476 updated_at_core_block_height: None,
477 transferred_at_core_block_height: None,
478 creator_id: None,
479 });
480
481 doc.increment_revision()
482 .expect("increment_revision should succeed");
483 assert_eq!(doc.revision(), Some(2));
484 }
485
486 #[test]
487 fn increment_revision_fails_when_no_revision() {
488 let mut doc = Document::V0(DocumentV0 {
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: None,
497 updated_at_block_height: None,
498 transferred_at_block_height: None,
499 created_at_core_block_height: None,
500 updated_at_core_block_height: None,
501 transferred_at_core_block_height: None,
502 creator_id: None,
503 });
504
505 let result = doc.increment_revision();
506 assert!(
507 result.is_err(),
508 "increment_revision should fail when revision is None"
509 );
510 }
511
512 #[test]
517 fn is_equal_ignoring_time_based_fields_dispatches_correctly() {
518 let platform_version = PlatformVersion::latest();
519 let contract = json_document_to_contract(
520 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
521 false,
522 platform_version,
523 )
524 .expect("expected to get contract");
525
526 let document_type = contract
527 .document_type_for_name("profile")
528 .expect("expected to get profile document type");
529
530 let doc1 = document_type
531 .random_document(Some(42), platform_version)
532 .expect("expected random document");
533
534 let mut doc2 = doc1.clone();
535 doc2.set_created_at(Some(9_999_999));
537 doc2.set_updated_at(Some(8_888_888));
538
539 let result = doc1
540 .is_equal_ignoring_time_based_fields(&doc2, None, platform_version)
541 .expect("should succeed");
542 assert!(
543 result,
544 "same document with different timestamps should be equal ignoring time fields"
545 );
546 }
547
548 #[test]
553 fn get_raw_for_contract_dispatches_to_v0() {
554 let platform_version = PlatformVersion::latest();
555 let contract = json_document_to_contract(
556 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
557 false,
558 platform_version,
559 )
560 .expect("expected to get contract");
561
562 let document_type = contract
563 .document_type_for_name("profile")
564 .expect("expected to get profile document type");
565
566 let document = document_type
567 .random_document(Some(7), platform_version)
568 .expect("expected random document");
569
570 let raw_id = document
571 .get_raw_for_contract("$id", "profile", &contract, None, platform_version)
572 .expect("should succeed");
573 assert_eq!(raw_id, Some(document.id().to_vec()));
574 }
575
576 #[test]
581 fn document_hash_is_deterministic() {
582 let platform_version = PlatformVersion::latest();
583 let contract = json_document_to_contract(
584 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
585 false,
586 platform_version,
587 )
588 .expect("expected to get contract");
589
590 let document_type = contract
591 .document_type_for_name("profile")
592 .expect("expected to get profile document type");
593
594 let document = document_type
595 .random_document(Some(42), platform_version)
596 .expect("expected random document");
597
598 let hash1 = document
599 .hash(&contract, document_type, platform_version)
600 .expect("hash should succeed");
601 let hash2 = document
602 .hash(&contract, document_type, platform_version)
603 .expect("hash should succeed");
604 assert_eq!(hash1, hash2, "hash should be deterministic");
605 assert!(!hash1.is_empty(), "hash should not be empty");
606 }
607}