p2panda_rs/document/traits.rs
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//! Interfaces for interactions for document-like structs.
4
5use crate::document::error::DocumentError;
6use crate::document::{
7 DocumentId, DocumentView, DocumentViewFields, DocumentViewId, DocumentViewValue,
8};
9use crate::identity::PublicKey;
10use crate::operation::traits::AsOperation;
11use crate::operation::{OperationId, OperationValue};
12use crate::schema::SchemaId;
13
14/// Trait representing an "document-like" struct.
15pub trait AsDocument {
16 /// Get the document id.
17 fn id(&self) -> &DocumentId;
18
19 /// Get the document view id.
20 fn view_id(&self) -> &DocumentViewId;
21
22 /// Get the document author's public key.
23 fn author(&self) -> &PublicKey;
24
25 /// Get the document schema.
26 fn schema_id(&self) -> &SchemaId;
27
28 /// Get the fields of this document.
29 fn fields(&self) -> Option<&DocumentViewFields>;
30
31 /// Update the view of this document.
32 fn update_view(&mut self, id: &DocumentViewId, view: Option<&DocumentViewFields>);
33
34 /// Returns true if this document has applied an UPDATE operation.
35 fn is_edited(&self) -> bool {
36 match self.fields() {
37 Some(fields) => fields.iter().any(|(_, document_view_value)| {
38 &DocumentId::new(document_view_value.id()) != self.id()
39 }),
40 None => true,
41 }
42 }
43
44 /// Returns true if this document has processed a DELETE operation.
45 fn is_deleted(&self) -> bool {
46 self.fields().is_none()
47 }
48
49 /// The current document view for this document. Returns None if this document
50 /// has been deleted.
51 fn view(&self) -> Option<DocumentView> {
52 self.fields()
53 .map(|fields| DocumentView::new(self.view_id(), fields))
54 }
55
56 /// Get the value for a field on this document.
57 fn get(&self, key: &str) -> Option<&OperationValue> {
58 if let Some(fields) = self.fields() {
59 return fields.get(key).map(|view_value| view_value.value());
60 }
61 None
62 }
63
64 /// Update a documents current view with a single operation.
65 ///
66 /// For the update to be successful the passed operation must refer to this documents' current
67 /// view id in it's previous field and must update a field which exists on this document.
68 fn commit<T: AsOperation>(
69 &mut self,
70 operation_id: &OperationId,
71 operation: &T,
72 ) -> Result<(), DocumentError> {
73 // Validate operation passed to commit.
74 if operation.is_create() {
75 return Err(DocumentError::CommitCreate);
76 }
77
78 if &operation.schema_id() != self.schema_id() {
79 return Err(DocumentError::InvalidSchemaId(operation_id.to_owned()));
80 }
81
82 // Unwrap as all other operation types contain `previous`.
83 let previous = operation.previous().unwrap();
84
85 if self.is_deleted() {
86 return Err(DocumentError::UpdateOnDeleted);
87 }
88
89 if self.view_id() != &previous {
90 return Err(DocumentError::PreviousDoesNotMatch(operation_id.to_owned()));
91 }
92
93 // We performed all validation commit the operation.
94 self.commit_unchecked(operation_id, operation);
95
96 Ok(())
97 }
98
99 /// Commit an new operation to the document without performing any validation.
100 fn commit_unchecked<T: AsOperation>(&mut self, operation_id: &OperationId, operation: &T) {
101 let next_fields = match operation.fields() {
102 // If the operation contains fields it's an UPDATE and so we want to apply the changes
103 // to the designated fields.
104 Some(fields) => {
105 // Get the current document fields, we can unwrap as we checked for deleted
106 // documents above.
107 let mut document_fields = self.fields().unwrap().to_owned();
108
109 // For every field in the UPDATE operation update the relevant field in the
110 // current document fields.
111 for (name, value) in fields.iter() {
112 let document_field_value = DocumentViewValue::new(operation_id, value);
113
114 // We know all the fields are correct for this document as we checked the
115 // schema id above.
116 document_fields.insert(name, document_field_value);
117 }
118
119 // Return the updated fields.
120 Some(document_fields)
121 }
122 // If the operation doesn't contain fields this must be a DELETE so we return None as we want to remove the
123 // current document's fields.
124 None => None,
125 };
126
127 // Construct the new document view id.
128 let document_view_id = DocumentViewId::new(&[operation_id.to_owned()]);
129
130 // Update the documents' view, edited/deleted state and view id.
131 self.update_view(&document_view_id, next_fields.as_ref());
132 }
133}