p2panda_rs/operation/
relation.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3use std::convert::TryFrom;
4use std::slice::Iter;
5
6use serde::{Deserialize, Serialize};
7
8use crate::document::error::DocumentIdError;
9use crate::document::{DocumentId, DocumentViewId};
10use crate::operation::error::{
11    PinnedRelationError, PinnedRelationListError, RelationError, RelationListError,
12};
13use crate::operation::OperationId;
14use crate::Validate;
15
16/// Field type representing references to other documents.
17///
18/// Relation types describe references to other documents.
19///
20/// Similar to SQL relationships, documents refer to one another by their _document id_. This module
21/// provides types used in operations to refer to one (`Relation`) or many documents
22/// (`RelationList`).
23///
24/// This is an example of a simple `Relation` where a _Comment_ Document refers to a _Blog Post_
25/// Document:
26///
27/// ```text
28/// Document: [Blog-Post "Monday evening"]
29///     ^
30///     |
31/// Document: [Comment "This was great!"]
32/// ```
33#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
34pub struct Relation(DocumentId);
35
36impl Relation {
37    /// Returns a new relation field.
38    pub fn new(document: DocumentId) -> Self {
39        Self(document)
40    }
41
42    /// Returns the relations document id.
43    pub fn document_id(&self) -> &DocumentId {
44        &self.0
45    }
46}
47
48impl Validate for Relation {
49    type Error = RelationError;
50
51    fn validate(&self) -> Result<(), Self::Error> {
52        self.0.validate()?;
53        Ok(())
54    }
55}
56
57impl<'de> Deserialize<'de> for Relation {
58    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59    where
60        D: serde::Deserializer<'de>,
61    {
62        // Deserialize into `DocumentId` struct
63        let document_id: DocumentId = Deserialize::deserialize(deserializer)?;
64
65        // Check format
66        document_id
67            .validate()
68            .map_err(|err| serde::de::Error::custom(format!("invalid document id, {}", err)))?;
69
70        Ok(Self(document_id))
71    }
72}
73
74/// Reference to the exact version of the document.
75///
76/// `PinnedRelation` _pin_ a relation to a specific, immutable version of a document or many
77/// documents when necessary (`PinnedRelation` or `PinnedRelationList`).
78///
79/// When the blog post from the `Relation` example changes its contents from _Monday evening_ to
80/// _Tuesday morning_ the comment would automatically refer to the new version as the comment
81/// refers to the document as a whole, including all future changes.
82///
83/// Since the comment was probably meant to be referring to Monday when it was created, we have to
84/// _pin_ it to the exact version of the blog post in order to preserve this meaning. A
85/// `PinnedRelation` achieves this by referring to the blog post's _document view id_:
86///
87/// ```text
88///                    Document-View                              Document-View
89///                         |                                           |
90/// Document: [Blog-Post "Monday evening"] -- UPDATE -- > [Blog-Post "Tuesday morning"]
91///                   ^
92///                   |
93///      _____________|  Pinned Relation (we will stay in the "past")
94///     |
95///     |
96/// Document: [Comment "This was great!"]
97/// ```
98///
99/// Document view ids contain the operation ids of the document graph tips, which is all the
100/// information we need to reliably recreate the document at this certain point in time.
101///
102/// Pinned relations give us immutability and the option to restore a historical state across
103/// documents. However, most cases will probably only need unpinned relations: For example when
104/// referring to a user-profile you probably want to always get the _latest_ version.
105#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
106pub struct PinnedRelation(DocumentViewId);
107
108impl PinnedRelation {
109    /// Returns a new pinned relation field.
110    pub fn new(document_view_id: DocumentViewId) -> Self {
111        Self(document_view_id)
112    }
113
114    /// Returns the pinned relation's document view id.
115    pub fn view_id(&self) -> &DocumentViewId {
116        &self.0
117    }
118
119    /// Returns iterator over operation ids.
120    pub fn iter(&self) -> Iter<OperationId> {
121        self.0.iter()
122    }
123}
124
125impl Validate for PinnedRelation {
126    type Error = PinnedRelationError;
127
128    fn validate(&self) -> Result<(), Self::Error> {
129        self.0.validate()?;
130        Ok(())
131    }
132}
133
134impl<'de> Deserialize<'de> for PinnedRelation {
135    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
136    where
137        D: serde::Deserializer<'de>,
138    {
139        // Deserialize into `DocumentViewId` struct
140        let document_view_id: DocumentViewId = Deserialize::deserialize(deserializer)?;
141
142        // Check format
143        document_view_id.validate().map_err(|err| {
144            serde::de::Error::custom(format!("invalid document view id, {}", err))
145        })?;
146
147        Ok(Self(document_view_id))
148    }
149}
150
151/// A `RelationList` can be used to reference multiple foreign documents from a document field.
152///
153/// The item order and occurrences inside a relation list are defined by the developers and users
154/// and have semantic meaning, for this reason we do not check against duplicates or ordering here.
155#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
156#[allow(clippy::len_without_is_empty)]
157pub struct RelationList(Vec<DocumentId>);
158
159impl RelationList {
160    /// Returns a new list of relations.
161    pub fn new(relations: Vec<DocumentId>) -> Self {
162        Self(relations)
163    }
164
165    /// Returns the list of document ids.
166    pub fn document_ids(&self) -> &[DocumentId] {
167        self.0.as_slice()
168    }
169
170    /// Returns iterator over document ids.
171    pub fn iter(&self) -> Iter<DocumentId> {
172        self.0.iter()
173    }
174
175    /// Returns number of documents in this relation list.
176    pub fn len(&self) -> usize {
177        self.0.len()
178    }
179}
180
181impl Validate for RelationList {
182    type Error = RelationListError;
183
184    fn validate(&self) -> Result<(), Self::Error> {
185        // Note that we do NOT check for duplicates and ordering here as this information is
186        // semantic!
187        for document_id in &self.0 {
188            document_id.validate()?;
189        }
190
191        Ok(())
192    }
193}
194
195impl TryFrom<&[String]> for RelationList {
196    type Error = RelationListError;
197
198    fn try_from(str_list: &[String]) -> Result<Self, Self::Error> {
199        let document_ids: Result<Vec<DocumentId>, DocumentIdError> = str_list
200            .iter()
201            .map(|document_id_str| document_id_str.parse::<DocumentId>())
202            .collect();
203
204        Ok(Self(document_ids?))
205    }
206}
207
208impl<'de> Deserialize<'de> for RelationList {
209    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
210    where
211        D: serde::Deserializer<'de>,
212    {
213        // Deserialize into `DocumentId` array
214        let document_ids: Vec<DocumentId> = Deserialize::deserialize(deserializer)?;
215
216        // Convert and check format
217        let relation_list = Self(document_ids);
218        relation_list
219            .validate()
220            .map_err(|err| serde::de::Error::custom(format!("invalid document id, {}", err)))?;
221
222        Ok(relation_list)
223    }
224}
225
226/// A `PinnedRelationList` can be used to reference multiple documents views.
227///
228/// The item order and occurrences inside a pinned relation list are defined by the developers and
229/// users and have semantic meaning, for this reason we do not check against duplicates or ordering
230/// here.
231#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
232#[allow(clippy::len_without_is_empty)]
233pub struct PinnedRelationList(Vec<DocumentViewId>);
234
235impl PinnedRelationList {
236    /// Returns a new list of pinned relations.
237    pub fn new(relations: Vec<DocumentViewId>) -> Self {
238        Self(relations)
239    }
240
241    /// Returns the list of document view ids.
242    pub fn document_view_ids(&self) -> &[DocumentViewId] {
243        self.0.as_slice()
244    }
245
246    /// Returns iterator over document view ids.
247    pub fn iter(&self) -> Iter<DocumentViewId> {
248        self.0.iter()
249    }
250
251    /// Returns number of pinned documents in this list.
252    pub fn len(&self) -> usize {
253        self.0.len()
254    }
255}
256
257impl Validate for PinnedRelationList {
258    type Error = PinnedRelationListError;
259
260    fn validate(&self) -> Result<(), Self::Error> {
261        // Note that we do NOT check for duplicates and ordering here as this information is
262        // semantic!
263        for document_view_id in &self.0 {
264            document_view_id.validate()?;
265        }
266
267        Ok(())
268    }
269}
270
271impl<'de> Deserialize<'de> for PinnedRelationList {
272    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
273    where
274        D: serde::Deserializer<'de>,
275    {
276        // Deserialize into `DocumentViewId` array
277        let document_view_ids: Vec<DocumentViewId> = Deserialize::deserialize(deserializer)?;
278
279        // Convert and check format
280        let pinned_relation_list = Self(document_view_ids);
281        pinned_relation_list.validate().map_err(|err| {
282            serde::de::Error::custom(format!("invalid document view id, {}", err))
283        })?;
284
285        Ok(pinned_relation_list)
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use std::str::FromStr;
292
293    use ciborium::cbor;
294    use rstest::rstest;
295
296    use crate::document::{DocumentId, DocumentViewId};
297    use crate::hash::Hash;
298    use crate::operation::OperationId;
299    use crate::serde::{deserialize_into, hex_string_to_bytes, serialize_from, serialize_value};
300    use crate::test_utils::fixtures::random_document_id;
301    use crate::test_utils::fixtures::random_hash;
302    use crate::Validate;
303
304    use super::{PinnedRelation, PinnedRelationList, Relation, RelationList};
305
306    #[rstest]
307    fn validation(
308        #[from(random_document_id)] document_1: DocumentId,
309        #[from(random_document_id)] document_2: DocumentId,
310        #[from(random_hash)] operation_id_1: Hash,
311        #[from(random_hash)] operation_id_2: Hash,
312    ) {
313        let relation = Relation::new(document_1.clone());
314        assert!(relation.validate().is_ok());
315
316        let pinned_relation = PinnedRelation::new(DocumentViewId::from(operation_id_1.clone()));
317        assert!(pinned_relation.validate().is_ok());
318
319        let relation_list = RelationList::new(vec![document_1, document_2]);
320        assert!(relation_list.validate().is_ok());
321
322        let pinned_relation_list =
323            PinnedRelationList::new(vec![operation_id_1.into(), operation_id_2.into()]);
324        assert!(pinned_relation_list.validate().is_ok());
325
326        let pinned_relation_list = PinnedRelationList::new(vec![]);
327        assert!(pinned_relation_list.validate().is_ok());
328    }
329
330    #[rstest]
331    fn iterates(#[from(random_hash)] hash_1: Hash, #[from(random_hash)] hash_2: Hash) {
332        let pinned_relation = PinnedRelation::new(DocumentViewId::new(&[
333            hash_1.clone().into(),
334            hash_2.clone().into(),
335        ]));
336
337        for hash in pinned_relation.iter() {
338            assert!(hash.validate().is_ok());
339        }
340
341        let relation_list = RelationList::new(vec![
342            DocumentId::new(&hash_1.clone().into()),
343            DocumentId::new(&hash_2.clone().into()),
344        ]);
345
346        for document_id in relation_list.iter() {
347            assert!(document_id.validate().is_ok());
348        }
349
350        let pinned_relation_list = PinnedRelationList::new(vec![
351            DocumentViewId::from(hash_1),
352            DocumentViewId::from(hash_2),
353        ]);
354
355        for pinned_relation in pinned_relation_list.iter() {
356            for hash in pinned_relation.graph_tips() {
357                assert!(hash.validate().is_ok());
358            }
359        }
360    }
361
362    #[rstest]
363    fn list_equality(
364        #[from(random_document_id)] document_1: DocumentId,
365        #[from(random_document_id)] document_2: DocumentId,
366        #[from(random_hash)] operation_id_1: Hash,
367        #[from(random_hash)] operation_id_2: Hash,
368    ) {
369        let relation_list = RelationList::new(vec![document_1.clone(), document_2.clone()]);
370        let relation_list_different_order = RelationList::new(vec![document_2, document_1]);
371        assert_ne!(relation_list, relation_list_different_order);
372
373        let pinned_relation_list = PinnedRelationList::new(vec![
374            operation_id_1.clone().into(),
375            operation_id_2.clone().into(),
376        ]);
377        let pinned_relation_list_different_order =
378            PinnedRelationList::new(vec![operation_id_2.into(), operation_id_1.into()]);
379        assert_ne!(pinned_relation_list, pinned_relation_list_different_order);
380    }
381
382    #[test]
383    fn serialize_relation() {
384        let hash_str = "0020b50b06774f909483c9c18e31b3bb17ff8f7d23088e9cc5a39260392259f34d42";
385        let bytes = serialize_from(Relation::new(DocumentId::from_str(hash_str).unwrap()));
386        assert_eq!(bytes, serialize_value(cbor!(hex_string_to_bytes(hash_str))));
387    }
388
389    #[test]
390    fn deserialize_relation() {
391        let hash_str = "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805";
392        let relation: Relation =
393            deserialize_into(&serialize_value(cbor!(hex_string_to_bytes(hash_str)))).unwrap();
394        assert_eq!(
395            Relation::new(DocumentId::from_str(hash_str).unwrap()),
396            relation
397        );
398
399        // Invalid hashes
400        let invalid_hash =
401            deserialize_into::<Relation>(&serialize_value(cbor!(hex_string_to_bytes("1234"))));
402        assert!(invalid_hash.is_err());
403        let empty_hash =
404            deserialize_into::<Relation>(&serialize_value(cbor!(hex_string_to_bytes(""))));
405        assert!(empty_hash.is_err());
406    }
407
408    #[test]
409    fn serialize_pinned_relation() {
410        let hash_str = "00208b050b24273b397f91a41e7f5030a853435dee0abbdc507dfc75a13809e7ba5f";
411        let bytes = serialize_from(PinnedRelation::new(
412            DocumentViewId::from_str(hash_str).unwrap(),
413        ));
414        assert_eq!(
415            bytes,
416            serialize_value(cbor!([hex_string_to_bytes(hash_str)]))
417        );
418    }
419
420    #[test]
421    fn deserialize_pinned_relation() {
422        let hash_str = "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805";
423        let pinned_relation: PinnedRelation =
424            deserialize_into(&serialize_value(cbor!([hex_string_to_bytes(
425                "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805"
426            )])))
427            .unwrap();
428        assert_eq!(
429            PinnedRelation::new(DocumentViewId::from_str(hash_str).unwrap()),
430            pinned_relation
431        );
432
433        // Invalid hashes
434        let invalid_hash =
435            deserialize_into::<PinnedRelation>(&serialize_value(cbor!([hex_string_to_bytes(
436                "1234"
437            )])));
438        assert!(invalid_hash.is_err());
439        let empty_hash = deserialize_into::<PinnedRelation>(&serialize_value(cbor!([])));
440        assert!(empty_hash.is_err());
441
442        // Invalid (non-canonic) order of operation ids
443        let unordered = deserialize_into::<PinnedRelation>(&serialize_value(cbor!([
444            "0020f1ab6d8114c0e7ab0af3bfd6862daf6ee0c510bbdf129e1780edfa505e860ff7",
445            "0020a19353e7dfeb2f9031087c3428a2467bb684e25321f09298c64ce1a2fd5787d1",
446        ])));
447        assert!(unordered.is_err());
448
449        // Duplicate operation ids
450        let duplicate = deserialize_into::<PinnedRelation>(&serialize_value(cbor!([
451            "05018634222cc8c9d49c5f48e8aecf0412c2cd2082a6712676373eaa1660e7af",
452            "05018634222cc8c9d49c5f48e8aecf0412c2cd2082a6712676373eaa1660e7af",
453        ])));
454        assert!(duplicate.is_err());
455    }
456
457    #[test]
458    fn serialize_relation_list() {
459        let hash_str = "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805";
460        let bytes = serialize_from(RelationList::new(vec![
461            DocumentId::from_str(hash_str).unwrap()
462        ]));
463        assert_eq!(
464            bytes,
465            serialize_value(cbor!([hex_string_to_bytes(hash_str)]))
466        );
467    }
468
469    #[test]
470    fn deserialize_relation_list() {
471        let hash_str_1 = "0020deb1356bcdec02e05ce4f1fce51561bbfda68d1c4537c98c592b9e2bf9917122";
472        let hash_str_2 = "002051044a3cfec6fea09759133dbae95dce9b49aa172df7fbb085c9b932694b2805";
473
474        let relation_list: RelationList = deserialize_into(&serialize_value(cbor!([
475            hex_string_to_bytes(hash_str_1),
476            hex_string_to_bytes(hash_str_2)
477        ])))
478        .unwrap();
479        assert_eq!(
480            RelationList::new(vec![
481                DocumentId::from_str(hash_str_1).unwrap(),
482                DocumentId::from_str(hash_str_2).unwrap()
483            ]),
484            relation_list
485        );
486
487        // Invalid hash
488        let invalid_hash =
489            deserialize_into::<RelationList>(&serialize_value(cbor!([hex_string_to_bytes(
490                "1234"
491            )])));
492        assert!(invalid_hash.is_err());
493    }
494
495    #[test]
496    fn serialize_pinned_relation_list() {
497        let hash_str_1 = "002051044a3cfec6fea09759133dbae95dce9b49aa172df7fbb085c9b932694b2805";
498        let hash_str_2 = "0020deb1356bcdec02e05ce4f1fce51561bbfda68d1c4537c98c592b9e2bf9917122";
499        let hash_str_3 = "002084d3c7eb7085c920879da6ea6c94cf89777e8f427a32f49d441fcda80cd39483";
500
501        let bytes = serialize_from(PinnedRelationList::new(vec![
502            DocumentViewId::new(&[
503                OperationId::from_str(hash_str_1).unwrap(),
504                OperationId::from_str(hash_str_2).unwrap(),
505            ]),
506            DocumentViewId::new(&[OperationId::from_str(hash_str_3).unwrap()]),
507        ]));
508        assert_eq!(
509            bytes,
510            serialize_value(cbor!([
511                [
512                    hex_string_to_bytes(hash_str_1),
513                    hex_string_to_bytes(hash_str_2)
514                ],
515                [hex_string_to_bytes(hash_str_3)]
516            ]))
517        );
518
519        let bytes = serialize_from(PinnedRelationList::new(vec![]));
520        assert_eq!(bytes, serialize_value(cbor!([])));
521    }
522
523    #[test]
524    fn deserialize_pinned_relation_list() {
525        let hash_str_1 = "002051044a3cfec6fea09759133dbae95dce9b49aa172df7fbb085c9b932694b2805";
526        let hash_str_2 = "0020deb1356bcdec02e05ce4f1fce51561bbfda68d1c4537c98c592b9e2bf9917122";
527        let hash_str_3 = "002084d3c7eb7085c920879da6ea6c94cf89777e8f427a32f49d441fcda80cd39483";
528
529        let pinned_relation_list: PinnedRelationList = deserialize_into(&serialize_value(cbor!([
530            [
531                hex_string_to_bytes(hash_str_1),
532                hex_string_to_bytes(hash_str_2)
533            ],
534            [hex_string_to_bytes(hash_str_3)]
535        ])))
536        .unwrap();
537        assert_eq!(
538            PinnedRelationList::new(vec![
539                DocumentViewId::new(&[
540                    OperationId::from_str(hash_str_1).unwrap(),
541                    OperationId::from_str(hash_str_2).unwrap(),
542                ]),
543                DocumentViewId::new(&[OperationId::from_str(hash_str_3).unwrap()]),
544            ]),
545            pinned_relation_list
546        );
547
548        let pinned_relation_list: PinnedRelationList =
549            deserialize_into(&serialize_value(cbor!([]))).unwrap();
550        assert_eq!(PinnedRelationList::new(vec![]), pinned_relation_list);
551
552        // Invalid hash
553        let invalid_hash = deserialize_into::<PinnedRelationList>(&serialize_value(cbor!([[
554            hex_string_to_bytes("1234")
555        ]])));
556        assert!(invalid_hash.is_err());
557
558        // Invalid (non-canonic) order of operation ids
559        let unordered = deserialize_into::<PinnedRelationList>(&serialize_value(cbor!([[
560            hex_string_to_bytes(
561                "0020f1ab6d8114c0e7ab0af3bfd6862daf6ee0c510bbdf129e1780edfa505e860ff7"
562            ),
563            hex_string_to_bytes(
564                "0020a19353e7dfeb2f9031087c3428a2467bb684e25321f09298c64ce1a2fd5787d1"
565            ),
566        ]])));
567        assert!(unordered.is_err());
568
569        // Duplicate operation ids
570        let duplicate = deserialize_into::<PinnedRelationList>(&serialize_value(cbor!([[
571            hex_string_to_bytes("05018634222cc8c9d49c5f48e8aecf0412c2cd2082a6712676373eaa1660e7af"),
572            hex_string_to_bytes("05018634222cc8c9d49c5f48e8aecf0412c2cd2082a6712676373eaa1660e7af"),
573        ]])));
574        assert!(duplicate.is_err());
575    }
576}