use crate::change::{Change, Changeset, FieldTag, FieldValue};
use crate::id::NodeId;
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UndoPayload {
Recreate {
stash: u32,
},
RestoreField {
field: FieldTag,
value: FieldValue,
},
RestorePending {
key: crate::reference::PendingRef,
slot: usize,
},
Remove,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UndoRecord {
pub node: NodeId,
pub payload: UndoPayload,
}
pub trait Undoable {
type Error;
fn undo(&mut self, record: &UndoRecord) -> Result<(), Self::Error>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TxnLog {
undo: Vec<UndoRecord>,
changes: Vec<Change>,
sequence: u64,
}
impl TxnLog {
#[must_use]
pub const fn new(sequence: u64) -> Self {
Self {
undo: Vec::new(),
changes: Vec::new(),
sequence,
}
}
#[must_use]
pub const fn sequence(&self) -> u64 {
self.sequence
}
pub fn record(&mut self, undo: UndoRecord, change: Change) {
self.undo.push(undo);
self.changes.push(change);
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.changes.len()
}
pub fn undo_records(&self) -> impl DoubleEndedIterator<Item = &UndoRecord> {
self.undo.iter()
}
pub fn changes(&self) -> impl Iterator<Item = &Change> {
self.changes.iter()
}
pub fn nodes_touched(&self) -> impl Iterator<Item = NodeId> + '_ {
self.changes.iter().map(Change::node)
}
#[must_use]
pub fn into_changeset(self) -> Changeset {
let mut cs = Changeset::new(self.sequence);
for change in self.changes {
cs.push(change);
}
cs
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
use crate::change::NodeKindTag;
use crate::id::IdSpace;
use alloc::vec;
use alloc::vec::Vec;
fn node(counter: u64) -> NodeId {
NodeId::from_parts(IdSpace::MODEL, counter)
}
#[test]
fn undo_records_come_back_in_reverse_application_order() {
let mut log = TxnLog::new(1);
log.record(
UndoRecord {
node: node(1),
payload: UndoPayload::Remove,
},
Change::Created {
node: node(1),
kind: NodeKindTag(1),
},
);
log.record(
UndoRecord {
node: node(1),
payload: UndoPayload::RestoreField {
field: FieldTag(0),
value: FieldValue::Absent,
},
},
Change::Updated {
node: node(1),
field: FieldTag(0),
before: FieldValue::Absent,
after: FieldValue::Bool(true),
},
);
let order: Vec<&UndoRecord> = log.undo_records().rev().collect();
assert!(matches!(order[0].payload, UndoPayload::RestoreField { .. }));
assert!(matches!(order[1].payload, UndoPayload::Remove));
}
#[test]
fn the_log_yields_a_changeset_in_application_order() {
let mut log = TxnLog::new(9);
log.record(
UndoRecord {
node: node(1),
payload: UndoPayload::Remove,
},
Change::Created {
node: node(1),
kind: NodeKindTag(1),
},
);
log.record(
UndoRecord {
node: node(2),
payload: UndoPayload::Remove,
},
Change::Created {
node: node(2),
kind: NodeKindTag(1),
},
);
assert_eq!(log.len(), 2);
let cs = log.into_changeset();
assert_eq!(cs.sequence(), 9);
assert_eq!(cs.len(), 2);
let nodes: Vec<NodeId> = cs.nodes_touched().collect();
assert_eq!(nodes, vec![node(1), node(2)]);
}
#[test]
fn an_empty_log_is_reported_as_empty() {
let log = TxnLog::new(0);
assert!(log.is_empty());
assert_eq!(log.len(), 0);
}
}