Skip to main content

blockworx/log/
command.rs

1//! The command vocabulary: the only things that can change a document.
2//!
3//! Every variant reduces to an LWW register write, a create, or a tombstone —
4//! the three primitives the convergence argument covers. A command type that
5//! does not reduce to them breaks the merge, so this is the place that
6//! constraint is enforced.
7
8use std::collections::BTreeMap;
9
10use serde::{Deserialize, Serialize};
11
12use crate::document::{
13    BlockLabel, GridPos, GridRect, LinearDistance, Lock, PinSide, PinType, TagVisibility, Waypoint,
14};
15use crate::log::id::{AssetId, ElementId};
16
17/// What kind of thing an element is. Fixed at creation and never written
18/// again, so it is not a register.
19#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
20pub enum ElementKind {
21    Block,
22    Pin,
23    Route,
24    Text,
25    Comment,
26    Image,
27    /// A block's foreground badge. Distinct from [`Image`](Self::Image), which
28    /// is a free-floating background annotation, because the two differ in
29    /// selectability and paint order rather than in content.
30    Icon,
31    RouteLabel,
32}
33
34/// A pin's location designator and whether it is drawn. One value because the
35/// flag governs the text — hiding a tag is not the same as clearing it.
36#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
37pub struct TagLabel {
38    pub text: String,
39    pub visibility: TagVisibility,
40}
41
42/// Where a pin sits on its owner's edge.
43#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
44pub struct PinPlacement {
45    pub side: PinSide,
46    pub offset: u32,
47}
48
49/// Declares the document's registers once, and derives from that single list:
50///
51/// - [`PropTag`] — which register, and the wire code identifying it;
52/// - [`PropWrite`] — a register's *value*, which is what state stores;
53/// - [`PropChange`] — a register's *transition*, which is what a command
54///   records.
55///
56/// The point is that the three cannot disagree. A transition is one variant
57/// carrying one type for both sides, so an edit whose old and new name
58/// different registers is unrepresentable rather than rejected — there is no
59/// tag to store beside the values and no constructor that has to check them.
60/// Adding a register is one line here.
61///
62/// Codes are persisted. They may be deprecated but never renumbered or reused.
63macro_rules! registers {
64    ($( $(#[$doc:meta])* $name:ident($ty:ty) = $code:literal ),+ $(,)?) => {
65        /// Which register a write targets.
66        #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
67        #[repr(u16)]
68        pub enum PropTag {
69            $( $(#[$doc])* $name = $code, )+
70        }
71
72        impl PropTag {
73            pub const fn code(self) -> u16 {
74                self as u16
75            }
76
77            pub const fn from_code(code: u16) -> Option<Self> {
78                match code {
79                    $( $code => Some(Self::$name), )+
80                    _ => None,
81                }
82            }
83        }
84
85        /// One register's value.
86        ///
87        /// No variant wraps an `Option`: "this block has no accent" is a
88        /// register holding nothing, not an `Accent(None)`. One state, one
89        /// representation.
90        #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
91        pub enum PropWrite {
92            $( $(#[$doc])* $name($ty), )+
93        }
94
95        impl PropWrite {
96            pub fn tag(&self) -> PropTag {
97                match self {
98                    $( Self::$name(_) => PropTag::$name, )+
99                }
100            }
101        }
102
103        /// What one register went from and to.
104        ///
105        /// `None` on a side means the register held nothing there — undo of a
106        /// first-time write clears rather than writing an "empty" value, which
107        /// is what makes it restore the document exactly.
108        #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
109        pub enum PropChange {
110            $( $(#[$doc])* $name { old: Option<$ty>, new: Option<$ty> }, )+
111        }
112
113        impl PropChange {
114            pub fn tag(&self) -> PropTag {
115                match self {
116                    $( Self::$name { .. } => PropTag::$name, )+
117                }
118            }
119
120            /// The value the author saw. Not needed to replay — it is here so a
121            /// command is self-contained for undo, for describing an edit, and
122            /// for noticing that someone else had already overwritten it.
123            pub fn old(&self) -> Option<PropWrite> {
124                match self {
125                    $( Self::$name { old, .. } => old.clone().map(PropWrite::$name), )+
126                }
127            }
128
129            /// The value to write.
130            pub fn new_value(&self) -> Option<PropWrite> {
131                match self {
132                    $( Self::$name { new, .. } => new.clone().map(PropWrite::$name), )+
133                }
134            }
135
136            /// Swapping the sides is the whole of undo for a property edit.
137            pub fn inverted(&self) -> Self {
138                match self {
139                    $( Self::$name { old, new } => Self::$name {
140                        old: new.clone(),
141                        new: old.clone(),
142                    }, )+
143                }
144            }
145        }
146    };
147}
148
149registers! {
150    /// The containment edge. A move is one write to this register, which is
151    /// why identity survives a move.
152    Parent(ElementId) = 1,
153    /// Block, comment, image and port geometry.
154    Rect(GridRect) = 2,
155    /// A text box's position. It has no size of its own — its extent is
156    /// whatever the text needs, which makes the extent derived, not authored.
157    Anchor(GridPos) = 3,
158    Title(BlockLabel) = 4,
159    TypeLabel(BlockLabel) = 5,
160    /// A pin's or route's primary label.
161    Name(String) = 6,
162    /// A pin's secondary label, drawn under its name.
163    TypeName(String) = 7,
164    Tag(TagLabel) = 8,
165    Placement(PinPlacement) = 9,
166    PinKind(PinType) = 10,
167    PortOrientation(PinSide) = 11,
168    /// Route endpoints are two registers rather than one keyed by which end,
169    /// so a change may repoint both ends without the second write landing on
170    /// the first.
171    RouteStart(ElementId) = 12,
172    RouteFinish(ElementId) = 13,
173    /// The whole polyline as one value: concurrent edits to a wire's shape
174    /// resolve as a unit rather than merging element-wise.
175    Waypoints(Vec<Waypoint>) = 14,
176    /// A route label's position along its wire.
177    Along(LinearDistance) = 15,
178    /// A text box's content.
179    Body(String) = 16,
180    Accent(u8) = 17,
181    Locked(Lock) = 18,
182    /// Content-derived, so referencing it from a command is legal.
183    Asset(AssetId) = 19,
184    DocumentName(String) = 20,
185}
186
187impl PropWrite {
188    /// The element this value points at, if it points at one. The repair pass
189    /// sweeps these for references to elements that are gone.
190    pub fn reference(&self) -> Option<ElementId> {
191        match self {
192            Self::Parent(id) | Self::RouteStart(id) | Self::RouteFinish(id) => Some(*id),
193            _ => None,
194        }
195    }
196}
197
198/// An element's initial registers. A map rather than a list so one create
199/// cannot carry two writes to the same register — they would share a
200/// [`WriteOrder`](crate::log::id::WriteOrder) and be unordered.
201#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
202#[serde(into = "Vec<PropWrite>", try_from = "Vec<PropWrite>")]
203pub struct PropSet(BTreeMap<PropTag, PropWrite>);
204
205impl From<PropSet> for Vec<PropWrite> {
206    fn from(set: PropSet) -> Self {
207        set.0.into_values().collect()
208    }
209}
210
211impl TryFrom<Vec<PropWrite>> for PropSet {
212    type Error = &'static str;
213
214    /// Refuses rather than repairs: collecting would silently drop a duplicate
215    /// register, which is the same "one value, many encodings" hazard as
216    /// unsorted parents.
217    fn try_from(writes: Vec<PropWrite>) -> Result<Self, Self::Error> {
218        let mut set = BTreeMap::new();
219        for write in writes {
220            if set.insert(write.tag(), write).is_some() {
221                return Err("a create names one register twice");
222            }
223        }
224        Ok(Self(set))
225    }
226}
227
228impl PropSet {
229    pub fn new() -> Self {
230        Self::default()
231    }
232
233    /// Later writes to a register replace earlier ones.
234    pub fn with(mut self, write: PropWrite) -> Self {
235        self.insert(write);
236        self
237    }
238
239    pub fn insert(&mut self, write: PropWrite) -> Option<PropWrite> {
240        self.0.insert(write.tag(), write)
241    }
242
243    pub fn get(&self, tag: PropTag) -> Option<&PropWrite> {
244        self.0.get(&tag)
245    }
246
247    pub fn len(&self) -> usize {
248        self.0.len()
249    }
250
251    pub fn is_empty(&self) -> bool {
252        self.0.is_empty()
253    }
254
255    /// Ordered by tag, so two replicas encode the same set to the same bytes.
256    pub fn iter(&self) -> impl Iterator<Item = &PropWrite> {
257        self.0.values()
258    }
259}
260
261impl FromIterator<PropWrite> for PropSet {
262    fn from_iter<I: IntoIterator<Item = PropWrite>>(iter: I) -> Self {
263        Self(iter.into_iter().map(|w| (w.tag(), w)).collect())
264    }
265}
266
267/// One primitive edit.
268///
269/// A high-level operation — align thirty blocks, duplicate a subtree — is
270/// recorded as the primitives it produced, with identities already minted and
271/// references already remapped. Replay must never re-run the logic that
272/// decided them, or two replicas replaying the same log could decide
273/// differently.
274#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
275pub enum Command {
276    Create {
277        id: ElementId,
278        kind: ElementKind,
279        parent: ElementId,
280        init: PropSet,
281    },
282    SetProp {
283        id: ElementId,
284        change: PropChange,
285    },
286    /// Tombstone. The element and its id persist so a restore can bring it
287    /// back, and so a late edit naming it can be reported rather than dropped.
288    Delete {
289        id: ElementId,
290    },
291    Restore {
292        id: ElementId,
293    },
294}
295
296impl Command {
297    pub fn target(&self) -> ElementId {
298        match self {
299            Self::Create { id, .. }
300            | Self::SetProp { id, .. }
301            | Self::Delete { id }
302            | Self::Restore { id } => *id,
303        }
304    }
305
306    /// The inverse edit. Every command inverts from its own fields alone,
307    /// which is what lets undo be an append rather than a rewind.
308    pub fn inverted(&self) -> Self {
309        match self {
310            Self::Delete { id } => Self::Restore { id: *id },
311            Self::Create { id, .. } | Self::Restore { id } => Self::Delete { id: *id },
312            Self::SetProp { id, change } => Self::SetProp {
313                id: *id,
314                change: change.inverted(),
315            },
316        }
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    /// The property the macro exists for. There is no way to write an edit
325    /// whose two sides name different registers — `PropChange::Accent { old:
326    /// Some(1), new: Some(PropWrite::Name(..)) }` does not typecheck — so
327    /// nothing has to check for it, at construction or on decode.
328    #[test]
329    fn a_property_edit_names_one_register_by_construction() {
330        let change = PropChange::Accent {
331            old: Some(1),
332            new: Some(3),
333        };
334        assert_eq!(change.tag(), PropTag::Accent);
335        assert_eq!(change.old(), Some(PropWrite::Accent(1)));
336        assert_eq!(change.new_value(), Some(PropWrite::Accent(3)));
337    }
338
339    #[test]
340    fn inverting_a_prop_change_swaps_its_sides() {
341        let change = PropChange::Accent {
342            old: Some(1),
343            new: Some(3),
344        };
345        let back = change.inverted();
346        assert_eq!(back.old(), change.new_value());
347        assert_eq!(back.new_value(), change.old());
348        assert_eq!(back.inverted(), change);
349    }
350
351    /// Giving a register its first value inverts into taking it away again,
352    /// not into writing an "empty" one. Both sides still name the register, so
353    /// the fold knows which one to clear.
354    #[test]
355    fn setting_a_first_value_inverts_into_clearing_it() {
356        let set = PropChange::Accent {
357            old: None,
358            new: Some(3),
359        };
360        let undo = set.inverted();
361
362        assert_eq!(undo.new_value(), None);
363        assert_eq!(undo.old(), Some(PropWrite::Accent(3)));
364        assert_eq!(
365            undo.tag(),
366            PropTag::Accent,
367            "a cleared register still names the register it clears"
368        );
369        assert_eq!(undo.inverted(), set);
370    }
371
372    #[test]
373    fn a_prop_set_holds_one_write_per_register() {
374        let set: PropSet = [
375            PropWrite::Name("first".into()),
376            PropWrite::Accent(1),
377            PropWrite::Name("second".into()),
378        ]
379        .into_iter()
380        .collect();
381
382        assert_eq!(
383            set.len(),
384            2,
385            "the duplicate register must not be kept twice"
386        );
387        assert_eq!(
388            set.get(PropTag::Name),
389            Some(&PropWrite::Name("second".into())),
390            "the later write should win"
391        );
392    }
393
394    /// The set is encoded in iteration order, so that order has to be a
395    /// function of content alone.
396    #[test]
397    fn a_prop_set_iterates_in_tag_order_whatever_the_build_order() {
398        let forwards: PropSet = [PropWrite::Parent(ElementId::DOCUMENT), PropWrite::Accent(1)]
399            .into_iter()
400            .collect();
401        let backwards: PropSet = [PropWrite::Accent(1), PropWrite::Parent(ElementId::DOCUMENT)]
402            .into_iter()
403            .collect();
404
405        let tags = |s: &PropSet| s.iter().map(PropWrite::tag).collect::<Vec<_>>();
406        assert_eq!(tags(&forwards), tags(&backwards));
407        assert_eq!(tags(&forwards), vec![PropTag::Parent, PropTag::Accent]);
408    }
409
410    /// Guards the deprecate-never-renumber rule. The macro generates the codes
411    /// and `from_code` from one list, so they cannot disagree — what this
412    /// checks is that nobody renumbered the list itself.
413    #[test]
414    fn every_tag_round_trips_through_its_code() {
415        for code in 1..=20u16 {
416            let tag = PropTag::from_code(code).expect("codes 1..=20 are all assigned");
417            assert_eq!(tag.code(), code);
418        }
419        assert!(PropTag::from_code(0).is_none());
420        assert!(PropTag::from_code(21).is_none());
421    }
422
423    #[test]
424    fn creates_and_deletes_invert_into_each_other() {
425        let id = ElementId::new();
426        let create = Command::Create {
427            id,
428            kind: ElementKind::Block,
429            parent: ElementId::DOCUMENT,
430            init: PropSet::new(),
431        };
432        assert_eq!(create.inverted(), Command::Delete { id });
433        assert_eq!(
434            Command::Delete { id }.inverted(),
435            Command::Restore { id },
436            "undoing a delete restores rather than recreating, so identity survives"
437        );
438    }
439}