#[cfg(feature = "history")]
use libro::{AuditChain, AuditEntry, EventSeverity};
#[cfg(feature = "history")]
use serde_json::Value;
#[cfg(feature = "history")]
use std::borrow::Cow;
#[cfg(feature = "history")]
#[derive(Debug, Clone)]
pub struct Action {
pub kind: Cow<'static, str>,
pub details: Value,
}
#[cfg(feature = "history")]
impl Action {
#[must_use]
#[inline]
pub fn new(kind: &'static str, details: Value) -> Self {
Self {
kind: Cow::Borrowed(kind),
details,
}
}
#[must_use]
#[inline]
pub fn with_kind(kind: String, details: Value) -> Self {
Self {
kind: Cow::Owned(kind),
details,
}
}
}
#[cfg(feature = "history")]
pub struct History {
chain: AuditChain,
cursor: usize,
}
#[cfg(feature = "history")]
impl History {
#[must_use]
pub fn new() -> Self {
Self {
chain: AuditChain::new(),
cursor: 0,
}
}
pub fn record(&mut self, source: &str, action: Action) {
let kind_ref: &str = &action.kind;
self.chain
.append(EventSeverity::Info, source, kind_ref, action.details);
self.cursor = self.chain.len();
tracing::debug!(
source,
kind = kind_ref,
cursor = self.cursor,
"action recorded"
);
}
#[must_use]
#[inline]
pub fn can_undo(&self) -> bool {
self.cursor > 0
}
#[must_use]
#[inline]
pub fn can_redo(&self) -> bool {
self.cursor < self.chain.len()
}
pub fn undo(&mut self) -> Option<&AuditEntry> {
if !self.can_undo() {
return None;
}
self.cursor -= 1;
tracing::debug!(cursor = self.cursor, "undo");
self.chain.entries().get(self.cursor)
}
pub fn redo(&mut self) -> Option<&AuditEntry> {
if !self.can_redo() {
return None;
}
let entry = self.chain.entries().get(self.cursor);
self.cursor += 1;
tracing::debug!(cursor = self.cursor, "redo");
entry
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.chain.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.chain.is_empty()
}
#[must_use]
#[inline]
pub fn cursor(&self) -> usize {
self.cursor
}
#[must_use]
pub fn verify(&self) -> bool {
self.chain.verify().is_ok()
}
#[must_use]
pub fn entries(&self) -> &[AuditEntry] {
self.chain.entries()
}
#[must_use]
pub fn applied_entries(&self) -> &[AuditEntry] {
let len = self.chain.len();
&self.chain.entries()[..self.cursor.min(len)]
}
#[must_use]
pub fn page(&self, offset: usize, limit: usize) -> &[AuditEntry] {
self.chain.page(offset, limit)
}
}
#[cfg(feature = "history")]
impl Default for History {
fn default() -> Self {
Self::new()
}
}
#[cfg(all(test, feature = "history"))]
mod tests {
use super::*;
use serde_json::json;
fn action(kind: &'static str) -> Action {
Action::new(kind, json!({"test": true}))
}
#[test]
fn new_history_is_empty() {
let h = History::new();
assert!(h.is_empty());
assert_eq!(h.len(), 0);
assert_eq!(h.cursor(), 0);
assert!(!h.can_undo());
assert!(!h.can_redo());
}
#[test]
fn record_increments_cursor() {
let mut h = History::new();
h.record("test", action("do_something"));
assert_eq!(h.len(), 1);
assert_eq!(h.cursor(), 1);
assert!(h.can_undo());
assert!(!h.can_redo());
}
#[test]
fn undo_moves_cursor_back() {
let mut h = History::new();
h.record("test", action("a"));
h.record("test", action("b"));
let entry = h.undo().unwrap();
assert_eq!(entry.action(), "b");
assert_eq!(h.cursor(), 1);
assert!(h.can_undo());
assert!(h.can_redo());
}
#[test]
fn redo_moves_cursor_forward() {
let mut h = History::new();
h.record("test", action("a"));
h.undo();
let entry = h.redo().unwrap();
assert_eq!(entry.action(), "a");
assert_eq!(h.cursor(), 1);
assert!(!h.can_redo());
}
#[test]
fn undo_at_start_returns_none() {
let mut h = History::new();
assert!(h.undo().is_none());
}
#[test]
fn redo_at_end_returns_none() {
let mut h = History::new();
h.record("test", action("a"));
assert!(h.redo().is_none());
}
#[test]
fn undo_redo_roundtrip() {
let mut h = History::new();
h.record("test", action("a"));
h.record("test", action("b"));
h.record("test", action("c"));
h.undo();
h.undo();
assert_eq!(h.cursor(), 1);
h.redo();
h.redo();
assert_eq!(h.cursor(), 3);
assert!(!h.can_redo());
}
#[test]
fn verify_intact_chain() {
let mut h = History::new();
h.record("editor", action("move"));
h.record("editor", action("rotate"));
assert!(h.verify());
}
#[test]
fn applied_entries_tracks_cursor() {
let mut h = History::new();
h.record("test", action("a"));
h.record("test", action("b"));
h.record("test", action("c"));
assert_eq!(h.applied_entries().len(), 3);
h.undo();
assert_eq!(h.applied_entries().len(), 2);
}
#[test]
fn page_returns_slice() {
let mut h = History::new();
for i in 0..10 {
h.record(
"test",
Action::with_kind(format!("action_{i}"), json!({"test": true})),
);
}
let page = h.page(5, 3);
assert_eq!(page.len(), 3);
}
#[test]
fn entries_preserves_details() {
let mut h = History::new();
let details = json!({"entity": 42, "before": [0, 0, 0], "after": [1, 2, 3]});
h.record("inspector", Action::new("set_position", details.clone()));
assert_eq!(h.entries()[0].details(), &details);
}
#[test]
fn record_after_undo_invalidates_redo() {
let mut h = History::new();
h.record("test", action("a"));
h.record("test", action("b"));
h.record("test", action("c"));
h.undo();
h.undo();
assert_eq!(h.cursor(), 1);
h.record("test", action("d"));
assert_eq!(h.cursor(), h.len());
assert!(!h.can_redo());
assert!(h.can_undo());
}
#[test]
fn applied_entries_empty_history() {
let h = History::new();
assert!(h.applied_entries().is_empty());
}
#[test]
fn default_trait() {
let h = History::default();
assert!(h.is_empty());
}
}