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}