Skip to main content

blockworx_doc/
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::{
8    block_model::{
9        Area, AreaUpdate, Asset, Block, BlockUpdate, Image, ImageUpdate, Pin, PinUpdate, Route,
10        RouteLabel, RouteLabelUpdate, RouteUpdate, Text, TextUpdate,
11    },
12    document::TitleBlockUpdate,
13    hash::AssetHash,
14    id::{AreaId, BlockId, ImageId, PinId, RouteId, RouteLabelId, TextId},
15};
16
17/// One entity kind's lifecycle: `E` creates it whole, `U` writes one of
18/// its fields, `Delete` removes it.
19#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
20pub enum Crud<E, U> {
21    Create(E),
22    Update(U),
23    Delete,
24}
25
26#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
27pub enum OpCodes {
28    /// Singleton: no id, no lifecycle — update only.
29    Document(TitleBlockUpdate),
30    Block(BlockId, Crud<Block, BlockUpdate>),
31    Pin(PinId, Crud<Pin, PinUpdate>),
32    Route(RouteId, Crud<Route, RouteUpdate>),
33    RouteLabel(RouteLabelId, Crud<RouteLabel, RouteLabelUpdate>),
34    Text(TextId, Crud<Text, TextUpdate>),
35    Area(AreaId, Crud<Area, AreaUpdate>),
36    Image(ImageId, Crud<Image, ImageUpdate>),
37    /// A payload under its own content hash. Deliberately not a [`Crud`]:
38    /// an asset has no registers to update and no lifecycle to set — it
39    /// either exists or is unreferenced, and reclaiming the unreferenced
40    /// belongs to compaction, not to an edit. Create-only makes duplicate
41    /// delivery and replay structural no-ops, since one hash can only ever
42    /// name one byte string.
43    Asset(AssetHash, Asset),
44}
45
46impl OpCodes {
47    /// The op named without its payload: kind, target, lifecycle. The
48    /// editor's mutation log narrates with this — a trace wants to know
49    /// which entity changed and how, never what the user typed or which
50    /// bytes an asset carries.
51    #[must_use]
52    pub fn narrate(&self) -> String {
53        format!("{} {}", self.target(), self.act())
54    }
55
56    /// The entity this op touches — the one id union
57    /// ([`EntityRef`](crate::id::EntityRef)), which is what a note anchors to
58    /// and what the spotlight frames.
59    pub fn target(&self) -> crate::id::EntityRef {
60        use crate::id::EntityRef;
61        match self {
62            OpCodes::Document(_) => EntityRef::Document,
63            OpCodes::Block(id, _) => EntityRef::Block(*id),
64            OpCodes::Pin(id, _) => EntityRef::Pin(*id),
65            OpCodes::Route(id, _) => EntityRef::Route(*id),
66            OpCodes::RouteLabel(id, _) => EntityRef::RouteLabel(*id),
67            OpCodes::Text(id, _) => EntityRef::Text(*id),
68            OpCodes::Area(id, _) => EntityRef::Area(*id),
69            OpCodes::Image(id, _) => EntityRef::Image(*id),
70            OpCodes::Asset(hash, _) => EntityRef::Asset(*hash),
71        }
72    }
73
74    /// What the op does to its target, in the narration's one-word
75    /// spelling. The document is update-only and an asset create-only,
76    /// so neither carries a `Crud` to read.
77    fn act(&self) -> &'static str {
78        fn crud<E, U>(op: &Crud<E, U>) -> &'static str {
79            match op {
80                Crud::Create(_) => "create",
81                Crud::Update(_) => "update",
82                Crud::Delete => "delete",
83            }
84        }
85        match self {
86            OpCodes::Document(_) => "update",
87            OpCodes::Asset(..) => "create",
88            OpCodes::Block(_, op) => crud(op),
89            OpCodes::Pin(_, op) => crud(op),
90            OpCodes::Route(_, op) => crud(op),
91            OpCodes::RouteLabel(_, op) => crud(op),
92            OpCodes::Text(_, op) => crud(op),
93            OpCodes::Area(_, op) => crud(op),
94            OpCodes::Image(_, op) => crud(op),
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    /// The mutation log must not become a channel for the document's
104    /// contents: narration names the entity and the lifecycle, never the
105    /// value written.
106    #[test]
107    fn narration_names_the_target_without_its_payload() {
108        use crate::block_model::TextUpdate;
109        use crate::fixtures::{block_id, text_id};
110        let secret = "the user's private note".to_owned();
111        let op = OpCodes::Text(text_id(9), Crud::Update(TextUpdate::Text(secret.clone())));
112        let line = op.narrate();
113        assert!(line.contains("text"), "the kind is named: {line}");
114        assert!(line.contains("update"), "the lifecycle is named: {line}");
115        assert!(
116            !line.contains(&secret),
117            "narration must not carry the payload: {line}"
118        );
119        assert!(
120            OpCodes::Block(block_id(1), Crud::Delete)
121                .narrate()
122                .contains("delete")
123        );
124    }
125}