p2panda_rs/document/
document_view.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//! Types and methods for deriving and maintaining materialised documents.
4use std::collections::btree_map::Iter as BTreeMapIter;
5use std::fmt::Display;
6
7use crate::document::{DocumentViewFields, DocumentViewId, DocumentViewValue};
8use crate::Human;
9
10type FieldKey = String;
11
12/// The materialised view of a `Document`. It's fields match the documents schema definition.
13///
14/// `DocumentViews` are immutable versions of a `Document`. They represent a document at a certain
15/// point in time.
16#[derive(Debug, PartialEq, Clone)]
17pub struct DocumentView {
18    /// Identifier of this document view.
19    pub(crate) id: DocumentViewId,
20
21    /// Materialized data held by this document view.
22    pub(crate) fields: DocumentViewFields,
23}
24
25impl DocumentView {
26    /// Construct a document view.
27    ///
28    /// Requires the DocumentViewId and field values to be calculated seperately and
29    /// then passed in during construction.
30    pub fn new(id: &DocumentViewId, fields: &DocumentViewFields) -> Self {
31        Self {
32            id: id.clone(),
33            fields: fields.clone(),
34        }
35    }
36
37    /// Get the id of this document view.
38    pub fn id(&self) -> &DocumentViewId {
39        &self.id
40    }
41
42    /// Get a single value from this instance by it's key.
43    pub fn get(&self, key: &str) -> Option<&DocumentViewValue> {
44        self.fields.get(key)
45    }
46
47    /// Returns a vector containing the keys of this instance.
48    pub fn keys(&self) -> Vec<String> {
49        self.fields.keys()
50    }
51
52    /// Returns an iterator of existing instance fields.
53    pub fn iter(&self) -> BTreeMapIter<FieldKey, DocumentViewValue> {
54        self.fields.iter()
55    }
56
57    /// Returns the number of fields on this instance.
58    pub fn len(&self) -> usize {
59        self.fields.len()
60    }
61
62    /// Returns true if the instance is empty, otherwise false.
63    pub fn is_empty(&self) -> bool {
64        self.fields.is_empty()
65    }
66
67    /// Returns the fields of this document view.
68    pub fn fields(&self) -> &DocumentViewFields {
69        &self.fields
70    }
71}
72
73impl Display for DocumentView {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(f, "{}", self.id)
76    }
77}
78
79impl Human for DocumentView {
80    fn display(&self) -> String {
81        format!("<DocumentView {}>", self.id.display())
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use rstest::rstest;
88
89    use crate::document::traits::AsDocument;
90    use crate::document::{DocumentBuilder, DocumentViewId, DocumentViewValue};
91    use crate::identity::PublicKey;
92    use crate::operation::traits::AsOperation;
93    use crate::operation::{
94        Operation, OperationAction, OperationBuilder, OperationFields, OperationId, OperationValue,
95    };
96    use crate::test_utils::fixtures::{
97        create_operation, operation_fields, public_key, random_operation_id,
98    };
99    use crate::Human;
100
101    #[rstest]
102    fn from_single_create_op(
103        create_operation: Operation,
104        #[from(random_operation_id)] id: OperationId,
105        public_key: PublicKey,
106        operation_fields: OperationFields,
107    ) {
108        let (document, _) = DocumentBuilder::new(vec![(id.clone(), create_operation, public_key)])
109            .build()
110            .unwrap();
111        let document_view = document.view().unwrap();
112
113        assert!(!document_view.is_empty());
114        assert_eq!(document_view.len(), 9);
115        assert_eq!(document_view.keys(), operation_fields.keys());
116        for key in operation_fields.keys() {
117            assert_eq!(
118                document_view.get(&key).unwrap(),
119                &DocumentViewValue::new(&id, operation_fields.get(&key).unwrap(),),
120            );
121        }
122    }
123
124    #[rstest]
125    fn with_update_op(create_operation: Operation, public_key: PublicKey) {
126        let create_id = random_operation_id();
127        let update_operation = OperationBuilder::new(&create_operation.schema_id())
128            .action(OperationAction::Update)
129            .fields(&[(
130                "username",
131                OperationValue::String("Panda Cafe!!!!".to_string()),
132            )])
133            .previous(&DocumentViewId::new(&[create_id.clone()]))
134            .build()
135            .unwrap();
136        let update_id = random_operation_id();
137
138        let operations = vec![
139            (create_id, create_operation, public_key),
140            (update_id.clone(), update_operation, public_key),
141        ];
142
143        let (document, _) = DocumentBuilder::new(operations).build().unwrap();
144        let document_view = document.view().unwrap();
145
146        assert_eq!(
147            document_view.get("username").unwrap(),
148            &DocumentViewValue::new(
149                &update_id,
150                &OperationValue::String("Panda Cafe!!!!".to_owned()),
151            )
152        );
153    }
154
155    #[rstest]
156    fn string_representation(create_operation: Operation, public_key: PublicKey) {
157        let id_1 = "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
158            .parse::<OperationId>()
159            .unwrap();
160        let id_2 = "0020d3235c8fe6f58608200851b83cd8482808eb81e4c6b4b17805bba57da9f16e79"
161            .parse::<OperationId>()
162            .unwrap();
163
164        let (document, _) =
165            DocumentBuilder::new(vec![(id_1.clone(), create_operation, public_key)])
166                .build()
167                .unwrap();
168
169        // Fabricate a view id just for testing.
170        let mut view = document.view().unwrap();
171        let view_id = DocumentViewId::new(&[id_1.clone(), id_2.clone()]);
172        view.id = view_id;
173
174        assert_eq!(format!("{id_1}_{id_2}"), view.to_string());
175        assert_eq!(view.display(), "<DocumentView 496543_f16e79>");
176    }
177}