Skip to main content

blockworx/doc_ng/
opcode.rs

1//! The opcode: one primitive edit, its target id bundled in. A commit's
2//! payload is a `Vec<OpCodes>`; an op's index in it is its `Seq` in the
3//! total write order. Rationale: `docs/doc-ng-design-notes.md`.
4
5use serde::{Deserialize, Serialize};
6
7use crate::doc_ng::{
8    block_model::{
9        BlockInit, BlockUpdate, CommentInit, CommentUpdate, ImageInit, ImageUpdate, PinInit,
10        PinUpdate, RouteInit, RouteLabelInit, RouteLabelUpdate, RouteUpdate, TextInit, TextUpdate,
11    },
12    document::TitleBlockUpdate,
13    id::{BlockId, CommentId, ImageId, PinId, RouteId, RouteLabelId, TextId},
14};
15
16/// One entity kind's lifecycle: `I` creates it whole, `U` updates one
17/// register, `Delete` tombstones (the inner is retained), `Restore` brings
18/// it back.
19#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
20pub enum Crud<I, U> {
21    Create(I),
22    Restore,
23    Update(U),
24    Delete,
25}
26
27impl<I, U> Crud<I, U> {
28    /// The baseline-free half of inversion: un-creating is tombstoning,
29    /// resurrecting is restoring — identity survives the round trip.
30    /// `Update` has none — ops carry only the new value, so the client
31    /// journal captures the displaced value at seal time and builds the
32    /// inverse update itself. Borrowing rather than consuming: the
33    /// inverses carry no data, so the journal need not clone an init it
34    /// is about to discard.
35    #[must_use]
36    pub fn invert_lifecycle(&self) -> Option<Crud<I, U>> {
37        match self {
38            Crud::Create(_) | Crud::Restore => Some(Crud::Delete),
39            Crud::Delete => Some(Crud::Restore),
40            Crud::Update(_) => None,
41        }
42    }
43}
44
45#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
46pub enum OpCodes {
47    /// Singleton: no id, no lifecycle — update only.
48    Document(TitleBlockUpdate),
49    Block(BlockId, Crud<BlockInit, BlockUpdate>),
50    Pin(PinId, Crud<PinInit, PinUpdate>),
51    Route(RouteId, Crud<RouteInit, RouteUpdate>),
52    RouteLabel(RouteLabelId, Crud<RouteLabelInit, RouteLabelUpdate>),
53    Text(TextId, Crud<TextInit, TextUpdate>),
54    Comment(CommentId, Crud<CommentInit, CommentUpdate>),
55    Image(ImageId, Crud<ImageInit, ImageUpdate>),
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    /// The asymmetry is deliberate: `Create` and `Restore` both invert to
63    /// `Delete`, and `Delete` to `Restore` — never back to `Create`, so
64    /// identity survives the round trip.
65    #[test]
66    fn lifecycle_inversion_is_asymmetric() {
67        assert!(matches!(
68            Crud::<u8, u8>::Create(1).invert_lifecycle(),
69            Some(Crud::Delete)
70        ));
71        assert!(matches!(
72            Crud::<u8, u8>::Restore.invert_lifecycle(),
73            Some(Crud::Delete)
74        ));
75        assert!(matches!(
76            Crud::<u8, u8>::Delete.invert_lifecycle(),
77            Some(Crud::Restore)
78        ));
79    }
80
81    /// An update's inverse needs the displaced value, which ops no longer
82    /// carry — only the journal can build it.
83    #[test]
84    fn an_update_has_no_baseline_free_inverse() {
85        assert!(Crud::<u8, u8>::Update(7).invert_lifecycle().is_none());
86    }
87}