p2panda_rs/document/
document.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3use std::convert::TryFrom;
4use std::fmt::{Debug, Display};
5
6use crate::document::error::{DocumentBuilderError, DocumentReducerError};
7use crate::document::traits::AsDocument;
8use crate::document::{DocumentId, DocumentViewFields, DocumentViewId};
9use crate::graph::{Graph, Reducer};
10use crate::hash::HashId;
11use crate::identity::PublicKey;
12use crate::operation::traits::{AsOperation, WithPublicKey};
13use crate::operation::{Operation, OperationId};
14use crate::schema::SchemaId;
15use crate::{Human, WithId};
16
17use super::error::DocumentError;
18
19/// High-level datatype representing data published to the p2panda network as key-value pairs.
20///
21/// Documents are multi-writer and have automatic conflict resolution strategies which produce deterministic
22/// state for any two replicas. The underlying structure which make this possible is a directed acyclic graph
23/// of [`Operation`]'s. To arrive at the current state of a document the graph is topologically sorted,
24/// with any branches being ordered according to the conflicting operations [`OperationId`]. Each operation's
25/// mutation is applied in order which results in a LWW (last write wins) resolution strategy.
26///
27/// All documents have an accompanying `Schema` which describes the shape of the data they will contain. Every
28/// operation should have been validated against this schema before being included in the graph.
29///
30/// Documents are constructed through the [`DocumentBuilder`] or by conversion from vectors of a type implementing
31/// the [`AsOperation`], [`WithId<OperationId>`] and [`WithPublicKey`].
32///
33/// To efficiently commit more operations to an already constructed document use the `commit`
34/// method. Any operations committed in this way must refer to the documents current view id in
35/// their `previous` field.
36///
37/// See module docs for example uses.
38#[derive(Debug, Clone)]
39pub struct Document {
40    /// The id for this document.
41    id: DocumentId,
42
43    /// The data this document contains as key-value pairs.
44    fields: Option<DocumentViewFields>,
45
46    /// The id of the schema this document follows.
47    schema_id: SchemaId,
48
49    /// The id of the current view of this document.
50    view_id: DocumentViewId,
51
52    /// The public key of the author who created this document.
53    author: PublicKey,
54}
55
56impl AsDocument for Document {
57    /// Get the document id.
58    fn id(&self) -> &DocumentId {
59        &self.id
60    }
61
62    /// Get the document view id.
63    fn view_id(&self) -> &DocumentViewId {
64        &self.view_id
65    }
66
67    /// Get the document author's public key.
68    fn author(&self) -> &PublicKey {
69        &self.author
70    }
71
72    /// Get the document schema.
73    fn schema_id(&self) -> &SchemaId {
74        &self.schema_id
75    }
76
77    /// Get the fields of this document.
78    fn fields(&self) -> Option<&DocumentViewFields> {
79        self.fields.as_ref()
80    }
81
82    /// Update the current view of this document.
83    fn update_view(&mut self, id: &DocumentViewId, view: Option<&DocumentViewFields>) {
84        self.view_id = id.to_owned();
85        self.fields = view.cloned();
86    }
87}
88
89impl Display for Document {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        write!(f, "{}", self.id)
92    }
93}
94
95impl Human for Document {
96    fn display(&self) -> String {
97        let offset = yasmf_hash::MAX_YAMF_HASH_SIZE * 2 - 6;
98        format!("<Document {}>", &self.id.as_str()[offset..])
99    }
100}
101
102impl<T> TryFrom<Vec<&T>> for Document
103where
104    T: AsOperation + WithId<OperationId> + WithPublicKey,
105{
106    type Error = DocumentBuilderError;
107
108    fn try_from(operations: Vec<&T>) -> Result<Self, Self::Error> {
109        let document_builder: DocumentBuilder = operations.into();
110        let (document, _) = document_builder.build()?;
111        Ok(document)
112    }
113}
114
115impl<T> TryFrom<&Vec<T>> for Document
116where
117    T: AsOperation + WithId<OperationId> + WithPublicKey,
118{
119    type Error = DocumentBuilderError;
120
121    fn try_from(operations: &Vec<T>) -> Result<Self, Self::Error> {
122        let document_builder: DocumentBuilder = operations.into();
123        let (document, _) = document_builder.build()?;
124        Ok(document)
125    }
126}
127
128/// Struct which implements a Reducer used during document building.
129#[derive(Debug, Default)]
130struct DocumentReducer {
131    document: Option<Document>,
132}
133
134/// Implementation of the `Reduce` trait for collections of authored operations.
135impl Reducer<(OperationId, Operation, PublicKey)> for DocumentReducer {
136    type Error = DocumentReducerError;
137
138    /// Combine a visited operation with the existing document.
139    fn combine(&mut self, value: &(OperationId, Operation, PublicKey)) -> Result<(), Self::Error> {
140        // Extract the values.
141        let (operation_id, operation, public_key) = value;
142
143        // Get the current document.
144        let document = self.document.clone();
145
146        match document {
147            // If it has already been instantiated perform the commit.
148            Some(mut document) => {
149                match document.commit(operation_id, operation) {
150                    Ok(_) => Ok(()),
151                    Err(err) => match err {
152                        DocumentError::PreviousDoesNotMatch(_) => {
153                            // We accept this error as we are reducing the document while walking
154                            // the operation graph in DocumentBuilder. In this situation the
155                            // operations are being visited in their topologically sorted order
156                            // and in the case of branches, `previous` may not match the documents
157                            // current document view id.
158                            //
159                            // Perform the commit in any case.
160                            document.commit_unchecked(operation_id, operation);
161                            Ok(())
162                        }
163                        // These errors are serious and we should signal that the reducing failed
164                        // by storing the error.
165                        err => Err(err),
166                    },
167                }?;
168                // Set the updated document.
169                self.document = Some(document);
170                Ok(())
171            }
172            // If the document wasn't instantiated yet, then do so.
173            None => {
174                // Error if this operation is _not_ a CREATE operation.
175                if !operation.is_create() {
176                    return Err(DocumentReducerError::FirstOperationNotCreate);
177                }
178
179                // Construct the document view fields.
180                let document_fields = DocumentViewFields::new_from_operation_fields(
181                    operation_id,
182                    &operation.fields().unwrap(),
183                );
184
185                // Construct the document.
186                let document = Document {
187                    id: operation_id.as_hash().clone().into(),
188                    fields: Some(document_fields),
189                    schema_id: operation.schema_id(),
190                    view_id: DocumentViewId::new(&[operation_id.to_owned()]),
191                    author: public_key.to_owned(),
192                };
193
194                // Set the newly instantiated document.
195                self.document = Some(document);
196                Ok(())
197            }
198        }
199    }
200}
201
202type PublishedOperation = (OperationId, Operation, PublicKey);
203type OperationGraph = Graph<OperationId, PublishedOperation>;
204
205/// A struct for building [documents][`Document`] from a collection of operations.
206#[derive(Debug, Clone)]
207pub struct DocumentBuilder(Vec<(OperationId, Operation, PublicKey)>);
208
209impl DocumentBuilder {
210    /// Instantiate a new `DocumentBuilder` from a collection of operations.
211    pub fn new(operations: Vec<(OperationId, Operation, PublicKey)>) -> Self {
212        Self(operations)
213    }
214
215    /// Get all unsorted operations for this document.
216    pub fn operations(&self) -> &Vec<PublishedOperation> {
217        &self.0
218    }
219
220    /// Validates all contained operations and builds the document.
221    ///
222    /// The returned document contains the latest resolved [document view][`DocumentView`].
223    ///
224    /// Validation checks the following:
225    /// - There is exactly one `CREATE` operation.
226    /// - All operations are causally connected to the root operation.
227    /// - All operations follow the same schema.
228    /// - No cycles exist in the graph.
229    pub fn build(&self) -> Result<(Document, Vec<PublishedOperation>), DocumentBuilderError> {
230        let mut graph = self.construct_graph()?;
231        self.reduce_document(&mut graph)
232    }
233
234    /// Validates all contained operations and builds the document up to the
235    /// requested [`DocumentViewId`].
236    ///
237    /// The returned document contains the requested [document view][`DocumentView`].
238    ///
239    /// Validation checks the following:
240    /// - There is exactly one `CREATE` operation.
241    /// - All operations are causally connected to the root operation.
242    /// - All operations follow the same schema.
243    /// - No cycles exist in the graph.
244    pub fn build_to_view_id(
245        &self,
246        document_view_id: DocumentViewId,
247    ) -> Result<(Document, Vec<PublishedOperation>), DocumentBuilderError> {
248        let mut graph = self.construct_graph()?;
249        // Trim the graph to the requested view..
250        graph = graph.trim(document_view_id.graph_tips())?;
251        self.reduce_document(&mut graph)
252    }
253
254    /// Construct the document graph.
255    fn construct_graph(&self) -> Result<OperationGraph, DocumentBuilderError> {
256        // Instantiate the graph.
257        let mut graph = Graph::new();
258
259        let mut create_seen = false;
260
261        // Add all operations to the graph.
262        for (id, operation, public_key) in &self.0 {
263            // Check if this is a create operation and we already saw one, this should trigger an error.
264            if operation.is_create() && create_seen {
265                return Err(DocumentBuilderError::MultipleCreateOperations);
266            };
267
268            // Set the operation_seen flag.
269            if operation.is_create() {
270                create_seen = true;
271            }
272
273            graph.add_node(id, (id.to_owned(), operation.to_owned(), *public_key));
274        }
275
276        // Add links between operations in the graph.
277        for (id, operation, _public_key) in &self.0 {
278            if let Some(previous) = operation.previous() {
279                for previous in previous.iter() {
280                    let success = graph.add_link(previous, id);
281                    if !success {
282                        return Err(DocumentBuilderError::InvalidOperationLink(id.to_owned()));
283                    }
284                }
285            }
286        }
287
288        Ok(graph)
289    }
290
291    /// Traverse the graph, visiting operations in their topologically sorted order and reduce
292    /// them into a single document.
293    fn reduce_document(
294        &self,
295        graph: &mut OperationGraph,
296    ) -> Result<(Document, Vec<PublishedOperation>), DocumentBuilderError> {
297        // Walk the graph, visiting nodes in their topologically sorted order.
298        //
299        // We pass in a DocumentReducer which will construct the document as nodes (which contain
300        // operations) are visited.
301        let mut document_reducer = DocumentReducer::default();
302        let graph_data = graph.reduce(&mut document_reducer)?;
303        let graph_tips: Vec<OperationId> = graph_data
304            .current_graph_tips()
305            .iter()
306            .map(|(id, _, _)| id.to_owned())
307            .collect();
308
309        // Unwrap the document as if no error occurred it should be there.
310        let mut document = document_reducer.document.unwrap();
311
312        // One remaining task is to set the current document view id of the document. This is
313        // required as the document reducer only knows about the operations it visits in their
314        // already sorted order. It doesn't know about the state of the graphs tips.
315        document.view_id = DocumentViewId::new(&graph_tips);
316
317        Ok((document, graph_data.sorted()))
318    }
319}
320
321impl<T> From<Vec<&T>> for DocumentBuilder
322where
323    T: AsOperation + WithId<OperationId> + WithPublicKey,
324{
325    fn from(operations: Vec<&T>) -> Self {
326        let operations = operations
327            .into_iter()
328            .map(|operation| {
329                (
330                    operation.id().to_owned(),
331                    operation.into(),
332                    operation.public_key().to_owned(),
333                )
334            })
335            .collect();
336
337        Self(operations)
338    }
339}
340
341impl<T> From<&Vec<T>> for DocumentBuilder
342where
343    T: AsOperation + WithId<OperationId> + WithPublicKey,
344{
345    fn from(operations: &Vec<T>) -> Self {
346        let operations = operations
347            .iter()
348            .map(|operation| {
349                (
350                    operation.id().to_owned(),
351                    operation.into(),
352                    operation.public_key().to_owned(),
353                )
354            })
355            .collect();
356
357        Self(operations)
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use std::convert::{TryFrom, TryInto};
364
365    use rstest::rstest;
366
367    use crate::document::traits::AsDocument;
368    use crate::document::{
369        Document, DocumentId, DocumentViewFields, DocumentViewId, DocumentViewValue,
370    };
371    use crate::entry::traits::AsEncodedEntry;
372    use crate::identity::KeyPair;
373    use crate::operation::{OperationAction, OperationBuilder, OperationId, OperationValue};
374    use crate::schema::{FieldType, Schema, SchemaId, SchemaName};
375    use crate::test_utils::constants::{self, PRIVATE_KEY};
376    use crate::test_utils::fixtures::{
377        operation, operation_fields, published_operation, random_document_view_id,
378        random_operation_id, schema,
379    };
380    use crate::test_utils::memory_store::helpers::send_to_store;
381    use crate::test_utils::memory_store::{MemoryStore, PublishedOperation};
382    use crate::{Human, WithId};
383
384    use super::DocumentBuilder;
385
386    #[rstest]
387    fn string_representation(#[from(published_operation)] operation: PublishedOperation) {
388        let document: Document = vec![&operation].try_into().unwrap();
389
390        assert_eq!(
391            document.to_string(),
392            "00207f8ffabff270f21098a457b900b4989b7272a6cb637f3c938b06be0a77b708ed"
393        );
394
395        // Short string representation
396        assert_eq!(document.display(), "<Document b708ed>");
397
398        // Make sure the id is matching
399        assert_eq!(
400            document.id().as_str(),
401            "00207f8ffabff270f21098a457b900b4989b7272a6cb637f3c938b06be0a77b708ed"
402        );
403    }
404
405    #[rstest]
406    #[tokio::test]
407    async fn resolve_documents(
408        #[with(vec![("name".to_string(), FieldType::String)])] schema: Schema,
409    ) {
410        let panda = KeyPair::from_private_key_str(
411            "ddcafe34db2625af34c8ba3cf35d46e23283d908c9848c8b43d1f5d0fde779ea",
412        )
413        .unwrap();
414
415        let penguin = KeyPair::from_private_key_str(
416            "1c86b2524b48f0ba86103cddc6bdfd87774ab77ab4c0ea989ed0eeab3d28827a",
417        )
418        .unwrap();
419
420        let store = MemoryStore::default();
421
422        // Panda publishes a CREATE operation.
423        // This instantiates a new document.
424        //
425        // DOCUMENT: [panda_1]
426
427        let panda_operation_1 = OperationBuilder::new(schema.id())
428            .action(OperationAction::Create)
429            .fields(&[("name", OperationValue::String("Panda Cafe".to_string()))])
430            .build()
431            .unwrap();
432
433        let (panda_entry_1, _) = send_to_store(&store, &panda_operation_1, &schema, &panda)
434            .await
435            .unwrap();
436
437        // Panda publishes an UPDATE operation.
438        // It contains the id of the previous operation in it's `previous` array
439        //
440        // DOCUMENT: [panda_1]<--[panda_2]
441        //
442
443        let panda_operation_2 = OperationBuilder::new(schema.id())
444            .action(OperationAction::Update)
445            .fields(&[("name", OperationValue::String("Panda Cafe!".to_string()))])
446            .previous(&panda_entry_1.hash().into())
447            .build()
448            .unwrap();
449
450        let (panda_entry_2, _) = send_to_store(&store, &panda_operation_2, &schema, &panda)
451            .await
452            .unwrap();
453
454        // Penguin publishes an update operation which creates a new branch in the graph.
455        // This is because they didn't know about Panda's second operation.
456        //
457        // DOCUMENT: [panda_1]<--[penguin_1]
458        //                    \----[panda_2]
459
460        let penguin_operation_1 = OperationBuilder::new(schema.id())
461            .action(OperationAction::Update)
462            .fields(&[(
463                "name",
464                OperationValue::String("Penguin Cafe!!!".to_string()),
465            )])
466            .previous(&panda_entry_1.hash().into())
467            .build()
468            .unwrap();
469
470        let (penguin_entry_1, _) = send_to_store(&store, &penguin_operation_1, &schema, &penguin)
471            .await
472            .unwrap();
473
474        // Penguin publishes a new operation while now being aware of the previous branching situation.
475        // Their `previous` field now contains 2 operation id's.
476        //
477        // DOCUMENT: [panda_1]<--[penguin_1]<---[penguin_2]
478        //                    \----[panda_2]<--/
479
480        let penguin_operation_2 = OperationBuilder::new(schema.id())
481            .action(OperationAction::Update)
482            .fields(&[(
483                "name",
484                OperationValue::String("Polar Bear Cafe".to_string()),
485            )])
486            .previous(&DocumentViewId::new(&[
487                penguin_entry_1.hash().into(),
488                panda_entry_2.hash().into(),
489            ]))
490            .build()
491            .unwrap();
492
493        let (penguin_entry_2, _) = send_to_store(&store, &penguin_operation_2, &schema, &penguin)
494            .await
495            .unwrap();
496
497        // Penguin publishes a new update operation which points at the current graph tip.
498        //
499        // DOCUMENT: [panda_1]<--[penguin_1]<---[penguin_2]<--[penguin_3]
500        //                    \----[panda_2]<--/
501
502        let penguin_operation_3 = OperationBuilder::new(schema.id())
503            .action(OperationAction::Update)
504            .fields(&[(
505                "name",
506                OperationValue::String("Polar Bear Cafe!!!!!!!!!!".to_string()),
507            )])
508            .previous(&penguin_entry_2.hash().into())
509            .build()
510            .unwrap();
511
512        let (penguin_entry_3, _) = send_to_store(&store, &penguin_operation_3, &schema, &penguin)
513            .await
514            .unwrap();
515
516        let operations = store.operations.lock().unwrap();
517        let operations = operations.values().collect::<Vec<&PublishedOperation>>();
518        let document = Document::try_from(operations.clone());
519
520        assert!(document.is_ok(), "{:#?}", document);
521
522        // Document should resolve to expected value
523        let document = document.unwrap();
524
525        let mut exp_result = DocumentViewFields::new();
526        exp_result.insert(
527            "name",
528            DocumentViewValue::new(
529                &penguin_entry_3.hash().into(),
530                &OperationValue::String("Polar Bear Cafe!!!!!!!!!!".to_string()),
531            ),
532        );
533
534        let document_id = DocumentId::new(&panda_entry_1.hash().into());
535        let expected_graph_tips: Vec<OperationId> = vec![penguin_entry_3.hash().into()];
536
537        assert_eq!(
538            document.fields().unwrap().get("name"),
539            exp_result.get("name")
540        );
541        assert!(document.is_edited());
542        assert!(!document.is_deleted());
543        assert_eq!(document.author(), &panda.public_key());
544        assert_eq!(document.schema_id(), schema.id());
545        assert_eq!(document.view_id().graph_tips(), expected_graph_tips);
546        assert_eq!(document.id(), &document_id);
547
548        // Multiple replicas receiving operations in different orders should resolve to same value.
549        let replica_1: Document = vec![
550            operations[4],
551            operations[3],
552            operations[2],
553            operations[1],
554            operations[0],
555        ]
556        .try_into()
557        .unwrap();
558
559        let replica_2: Document = vec![
560            operations[2],
561            operations[1],
562            operations[0],
563            operations[4],
564            operations[3],
565        ]
566        .try_into()
567        .unwrap();
568
569        assert_eq!(
570            replica_1.fields().unwrap().get("name"),
571            exp_result.get("name")
572        );
573        assert!(replica_1.is_edited());
574        assert!(!replica_1.is_deleted());
575        assert_eq!(replica_1.author(), &panda.public_key());
576        assert_eq!(replica_1.schema_id(), schema.id());
577        assert_eq!(replica_1.view_id().graph_tips(), expected_graph_tips);
578        assert_eq!(replica_1.id(), &document_id);
579
580        assert_eq!(
581            replica_1.fields().unwrap().get("name"),
582            replica_2.fields().unwrap().get("name")
583        );
584        assert_eq!(replica_1.id(), replica_2.id());
585        assert_eq!(
586            replica_1.view_id().graph_tips(),
587            replica_2.view_id().graph_tips(),
588        );
589    }
590
591    #[rstest]
592    fn must_have_create_operation(
593        #[from(published_operation)]
594        #[with(
595            Some(operation_fields(constants::test_fields())),
596            constants::schema(),
597            Some(random_document_view_id())
598        )]
599        update_operation: PublishedOperation,
600    ) {
601        let document: Result<Document, _> = vec![&update_operation].try_into();
602        assert_eq!(
603            document.unwrap_err().to_string(),
604            format!(
605                "operation {} cannot be connected to the document graph",
606                WithId::<OperationId>::id(&update_operation)
607            )
608        );
609    }
610
611    #[rstest]
612    #[tokio::test]
613    async fn incorrect_previous_operations(
614        #[from(published_operation)]
615        #[with(Some(operation_fields(constants::test_fields())), constants::schema())]
616        create_operation: PublishedOperation,
617        #[from(published_operation)]
618        #[with(
619            Some(operation_fields(constants::test_fields())),
620            constants::schema(),
621            Some(random_document_view_id())
622        )]
623        update_operation: PublishedOperation,
624    ) {
625        let document: Result<Document, _> = vec![&create_operation, &update_operation].try_into();
626
627        assert_eq!(
628            document.unwrap_err().to_string(),
629            format!(
630                "operation {} cannot be connected to the document graph",
631                WithId::<OperationId>::id(&update_operation).clone()
632            )
633        );
634    }
635
636    #[rstest]
637    #[tokio::test]
638    async fn operation_schemas_not_matching() {
639        let create_operation = published_operation(
640            Some(operation_fields(constants::test_fields())),
641            constants::schema(),
642            None,
643            KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
644        );
645
646        let update_operation = published_operation(
647            Some(operation_fields(vec![
648                ("name", "is_cute".into()),
649                ("type", "bool".into()),
650            ])),
651            Schema::get_system(SchemaId::SchemaFieldDefinition(1))
652                .unwrap()
653                .to_owned(),
654            Some(WithId::<OperationId>::id(&create_operation).clone().into()),
655            KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
656        );
657
658        let document: Result<Document, _> = vec![&create_operation, &update_operation].try_into();
659
660        assert_eq!(
661            document.unwrap_err().to_string(),
662            "Could not perform reducer function: Operation 0020b7674a56756183f7d2c6afa20e06041a9a9a30b0aec728e35acf281ecff2b544 does not match the documents schema".to_string()
663        );
664    }
665
666    #[rstest]
667    #[tokio::test]
668    async fn is_deleted(
669        #[from(published_operation)]
670        #[with(Some(operation_fields(constants::test_fields())), constants::schema())]
671        create_operation: PublishedOperation,
672    ) {
673        let delete_operation = published_operation(
674            None,
675            constants::schema(),
676            Some(DocumentViewId::new(&[WithId::<OperationId>::id(
677                &create_operation,
678            )
679            .clone()])),
680            KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
681        );
682
683        let document: Document = vec![&create_operation, &delete_operation]
684            .try_into()
685            .unwrap();
686
687        assert!(document.is_deleted());
688        assert!(document.fields().is_none());
689    }
690
691    #[rstest]
692    #[tokio::test]
693    async fn more_than_one_create(
694        #[from(published_operation)] create_operation: PublishedOperation,
695    ) {
696        let document: Result<Document, _> = vec![&create_operation, &create_operation].try_into();
697
698        assert_eq!(
699            document.unwrap_err().to_string(),
700            "multiple CREATE operations found when building operation graph".to_string()
701        );
702    }
703
704    #[rstest]
705    #[tokio::test]
706    async fn fields(#[with(vec![("name".to_string(), FieldType::String)])] schema: Schema) {
707        let mut operations = Vec::new();
708
709        let panda = KeyPair::new().public_key().to_owned();
710        let penguin = KeyPair::new().public_key().to_owned();
711
712        // Panda publishes a CREATE operation.
713        // This instantiates a new document.
714        //
715        // DOCUMENT: [panda_1]
716
717        let operation_1_id = random_operation_id();
718        let operation = OperationBuilder::new(schema.id())
719            .action(OperationAction::Create)
720            .fields(&[("name", OperationValue::String("Panda Cafe".to_string()))])
721            .build()
722            .unwrap();
723
724        operations.push((operation_1_id.clone(), operation, panda));
725
726        // Panda publishes an UPDATE operation.
727        // It contains the id of the previous operation in it's `previous` array
728        //
729        // DOCUMENT: [panda_1]<--[panda_2]
730        //
731
732        let operation_2_id = random_operation_id();
733        let operation = OperationBuilder::new(schema.id())
734            .action(OperationAction::Update)
735            .fields(&[("name", OperationValue::String("Panda Cafe!".to_string()))])
736            .previous(&DocumentViewId::new(&[operation_1_id.clone()]))
737            .build()
738            .unwrap();
739
740        operations.push((operation_2_id.clone(), operation, panda));
741
742        // Penguin publishes an update operation which creates a new branch in the graph.
743        // This is because they didn't know about Panda's second operation.
744        //
745        // DOCUMENT: [panda_1]<--[penguin_1]
746        //                    \----[panda_2]
747
748        let operation_3_id = random_operation_id();
749        let operation = OperationBuilder::new(schema.id())
750            .action(OperationAction::Update)
751            .fields(&[(
752                "name",
753                OperationValue::String("Penguin Cafe!!!".to_string()),
754            )])
755            .previous(&DocumentViewId::new(&[operation_2_id.clone()]))
756            .build()
757            .unwrap();
758
759        operations.push((operation_3_id.clone(), operation, penguin));
760
761        let document_builder = DocumentBuilder::new(operations);
762
763        let (document, _) = document_builder
764            .build_to_view_id(DocumentViewId::new(&[operation_1_id]))
765            .unwrap();
766        assert_eq!(
767            document.fields().unwrap().get("name").unwrap().value(),
768            &OperationValue::String("Panda Cafe".to_string())
769        );
770
771        let (document, _) = document_builder
772            .build_to_view_id(DocumentViewId::new(&[operation_2_id.clone()]))
773            .unwrap();
774        assert_eq!(
775            document.fields().unwrap().get("name").unwrap().value(),
776            &OperationValue::String("Panda Cafe!".to_string())
777        );
778
779        let (document, _) = document_builder
780            .build_to_view_id(DocumentViewId::new(&[operation_3_id.clone()]))
781            .unwrap();
782        assert_eq!(
783            document.fields().unwrap().get("name").unwrap().value(),
784            &OperationValue::String("Penguin Cafe!!!".to_string())
785        );
786
787        let (document, _) = document_builder
788            .build_to_view_id(DocumentViewId::new(&[operation_2_id, operation_3_id]))
789            .unwrap();
790        assert_eq!(
791            document.fields().unwrap().get("name").unwrap().value(),
792            &OperationValue::String("Penguin Cafe!!!".to_string())
793        );
794    }
795
796    #[rstest]
797    #[tokio::test]
798    async fn apply_commit(
799        #[from(published_operation)]
800        #[with(Some(operation_fields(constants::test_fields())), constants::schema())]
801        create_operation: PublishedOperation,
802    ) {
803        // Construct operations we will use to update an existing document.
804
805        let create_view_id =
806            DocumentViewId::new(&[WithId::<OperationId>::id(&create_operation).clone()]);
807
808        let update_operation = operation(
809            Some(operation_fields(vec![("age", OperationValue::Integer(21))])),
810            Some(create_view_id.clone()),
811            constants::schema().id().to_owned(),
812        );
813
814        let update_operation_id = random_operation_id();
815        let update_view_id = DocumentViewId::new(&[update_operation_id.clone()]);
816
817        let delete_operation = operation(
818            None,
819            Some(update_view_id.clone()),
820            constants::schema().id().to_owned(),
821        );
822
823        let delete_operation_id = random_operation_id();
824        let delete_view_id = DocumentViewId::new(&[delete_operation_id.clone()]);
825
826        // Create the initial document from a single CREATE operation.
827        let mut document: Document = vec![&create_operation].try_into().unwrap();
828
829        assert!(!document.is_edited());
830        assert_eq!(document.view_id(), &create_view_id);
831        assert_eq!(document.get("age").unwrap(), &OperationValue::Integer(28));
832
833        // Apply a commit with an UPDATE operation.
834        document
835            .commit(&update_operation_id, &update_operation)
836            .unwrap();
837
838        assert!(document.is_edited());
839        assert_eq!(document.view_id(), &update_view_id);
840        assert_eq!(document.get("age").unwrap(), &OperationValue::Integer(21));
841
842        // Apply a commit with a DELETE operation.
843        document
844            .commit(&delete_operation_id, &delete_operation)
845            .unwrap();
846
847        assert!(document.is_deleted());
848        assert_eq!(document.view_id(), &delete_view_id);
849        assert_eq!(document.fields(), None);
850    }
851
852    #[rstest]
853    #[tokio::test]
854    async fn validate_commit_operation(
855        #[from(published_operation)]
856        #[with(Some(operation_fields(constants::test_fields())), constants::schema())]
857        create_operation: PublishedOperation,
858    ) {
859        // Create the initial document from a single CREATE operation.
860        let mut document: Document = vec![&create_operation].try_into().unwrap();
861
862        // Committing a CREATE operation should fail.
863        assert!(document
864            .commit(create_operation.id(), &create_operation)
865            .is_err());
866
867        let create_view_id =
868            DocumentViewId::new(&[WithId::<OperationId>::id(&create_operation).clone()]);
869
870        let schema_name = SchemaName::new("my_wrong_schema").expect("Valid schema name");
871        let update_with_incorrect_schema_id = published_operation(
872            Some(operation_fields(vec![("age", OperationValue::Integer(21))])),
873            schema(
874                vec![("age".into(), FieldType::Integer)],
875                SchemaId::new_application(&schema_name, &random_document_view_id()),
876                "Schema with a wrong id",
877            ),
878            Some(create_view_id.clone()),
879            KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
880        );
881
882        // Apply a commit with an UPDATE operation containing the wrong schema id.
883        assert!(document
884            .commit(
885                update_with_incorrect_schema_id.id(),
886                &update_with_incorrect_schema_id
887            )
888            .is_err());
889
890        let update_not_referring_to_current_view = published_operation(
891            Some(operation_fields(vec![("age", OperationValue::Integer(21))])),
892            constants::schema(),
893            Some(random_document_view_id()),
894            KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
895        );
896
897        // Apply a commit with an UPDATE operation not pointing to the current view.
898        assert!(document
899            .commit(
900                update_not_referring_to_current_view.id(),
901                &update_not_referring_to_current_view
902            )
903            .is_err());
904
905        // Now we apply a correct delete operation.
906        let delete_operation = published_operation(
907            None,
908            constants::schema(),
909            Some(create_view_id.clone()),
910            KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
911        );
912
913        assert!(document
914            .commit(delete_operation.id(), &delete_operation)
915            .is_ok());
916
917        let delete_view_id =
918            DocumentViewId::new(&[WithId::<OperationId>::id(&delete_operation).clone()]);
919
920        let update_on_a_deleted_document = published_operation(
921            Some(operation_fields(vec![("age", OperationValue::Integer(21))])),
922            constants::schema(),
923            Some(delete_view_id.to_owned()),
924            KeyPair::from_private_key_str(PRIVATE_KEY).unwrap(),
925        );
926
927        // Apply a commit with an UPDATE operation on a deleted document.
928        assert!(document
929            .commit(
930                update_on_a_deleted_document.id(),
931                &update_on_a_deleted_document
932            )
933            .is_err());
934    }
935}