use std::collections::VecDeque;
use std::time::SystemTime;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EventType {
Selection,
Visibility,
Lock,
DragDrop,
Rename,
Custom(String),
}
impl EventType {
pub fn as_str(&self) -> &str {
match self {
EventType::Selection => "Selection",
EventType::Visibility => "Visibility",
EventType::Lock => "Lock",
EventType::DragDrop => "DragDrop",
EventType::Rename => "Rename",
EventType::Custom(s) => s.as_str(),
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LogEntry<Id> {
pub timestamp: SystemTime,
pub message: String,
pub event_type: EventType,
pub node_id: Option<Id>,
}
impl<Id> LogEntry<Id> {
pub fn new(message: String, event_type: EventType, node_id: Option<Id>) -> Self {
Self {
timestamp: SystemTime::now(),
message,
event_type,
node_id,
}
}
pub fn event_type_str(&self) -> &str {
self.event_type.as_str()
}
pub fn elapsed(&self) -> Result<std::time::Duration, std::time::SystemTimeError> {
self.timestamp.elapsed()
}
pub fn format_elapsed(&self) -> String {
match self.elapsed() {
Ok(duration) => {
let secs = duration.as_secs();
if secs < 60 {
format!("{}s ago", secs)
} else if secs < 3600 {
format!("{}m ago", secs / 60)
} else if secs < 86400 {
format!("{}h ago", secs / 3600)
} else {
format!("{}d ago", secs / 86400)
}
}
Err(_) => "unknown".to_string(),
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EventLog<Id> {
entries: VecDeque<LogEntry<Id>>,
max_entries: usize,
}
impl<Id> EventLog<Id> {
pub fn new(max_entries: usize) -> Self {
Self {
entries: VecDeque::with_capacity(max_entries),
max_entries,
}
}
pub fn log(&mut self, message: impl Into<String>, event_type: EventType, node_id: Option<Id>) {
self.entries.push_front(LogEntry::new(
message.into(),
event_type,
node_id,
));
if self.entries.len() > self.max_entries {
self.entries.pop_back();
}
}
pub fn entries(&self) -> impl Iterator<Item = &LogEntry<Id>> {
self.entries.iter()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn max_entries(&self) -> usize {
self.max_entries
}
pub fn set_max_entries(&mut self, max_entries: usize) {
self.max_entries = max_entries;
while self.entries.len() > max_entries {
self.entries.pop_back();
}
}
pub fn filter_by_type(&self, event_type: &EventType) -> impl Iterator<Item = &LogEntry<Id>> {
self.entries.iter().filter(move |entry| &entry.event_type == event_type)
}
}
impl<Id> Default for EventLog<Id> {
fn default() -> Self {
Self::new(100)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_type_as_str() {
assert_eq!(EventType::Selection.as_str(), "Selection");
assert_eq!(EventType::Visibility.as_str(), "Visibility");
assert_eq!(EventType::Lock.as_str(), "Lock");
assert_eq!(EventType::DragDrop.as_str(), "DragDrop");
assert_eq!(EventType::Rename.as_str(), "Rename");
assert_eq!(EventType::Custom("Test".into()).as_str(), "Test");
}
#[test]
fn test_log_entry_creation() {
let entry = LogEntry::new(
"Test message".to_string(),
EventType::Selection,
Some(42u64),
);
assert_eq!(entry.message, "Test message");
assert_eq!(entry.event_type, EventType::Selection);
assert_eq!(entry.node_id, Some(42));
}
#[test]
fn test_log_entry_event_type_str() {
let entry = LogEntry::<u64>::new(
"Test".into(),
EventType::Rename,
None,
);
assert_eq!(entry.event_type_str(), "Rename");
}
#[test]
fn test_event_log_new() {
let log = EventLog::<u64>::new(10);
assert_eq!(log.max_entries(), 10);
assert_eq!(log.len(), 0);
assert!(log.is_empty());
}
#[test]
fn test_event_log_log() {
let mut log = EventLog::new(10);
log.log("Event 1", EventType::Selection, Some(1u64));
assert_eq!(log.len(), 1);
log.log("Event 2", EventType::Rename, Some(2u64));
assert_eq!(log.len(), 2);
}
#[test]
fn test_event_log_max_capacity() {
let mut log = EventLog::new(3);
log.log("Event 1", EventType::Selection, Some(1u64));
log.log("Event 2", EventType::Selection, Some(2u64));
log.log("Event 3", EventType::Selection, Some(3u64));
log.log("Event 4", EventType::Selection, Some(4u64));
assert_eq!(log.len(), 3);
let entries: Vec<_> = log.entries().collect();
assert_eq!(entries[0].message, "Event 4");
assert_eq!(entries[1].message, "Event 3");
assert_eq!(entries[2].message, "Event 2");
}
#[test]
fn test_event_log_clear() {
let mut log = EventLog::<u64>::new(10);
log.log("Event 1", EventType::Selection, None);
log.log("Event 2", EventType::Selection, None);
assert_eq!(log.len(), 2);
log.clear();
assert_eq!(log.len(), 0);
assert!(log.is_empty());
}
#[test]
fn test_event_log_set_max_entries() {
let mut log = EventLog::new(10);
for i in 0..10 {
log.log(format!("Event {}", i), EventType::Selection, Some(i));
}
assert_eq!(log.len(), 10);
log.set_max_entries(5);
assert_eq!(log.len(), 5);
assert_eq!(log.max_entries(), 5);
let entries: Vec<_> = log.entries().collect();
assert_eq!(entries[0].node_id, Some(9));
assert_eq!(entries[4].node_id, Some(5));
}
#[test]
fn test_event_log_filter_by_type() {
let mut log = EventLog::new(10);
log.log("Select 1", EventType::Selection, Some(1u64));
log.log("Rename 1", EventType::Rename, Some(2u64));
log.log("Select 2", EventType::Selection, Some(3u64));
log.log("Lock 1", EventType::Lock, Some(4u64));
log.log("Select 3", EventType::Selection, Some(5u64));
let selections: Vec<_> = log.filter_by_type(&EventType::Selection).collect();
assert_eq!(selections.len(), 3);
let renames: Vec<_> = log.filter_by_type(&EventType::Rename).collect();
assert_eq!(renames.len(), 1);
let visibility: Vec<_> = log.filter_by_type(&EventType::Visibility).collect();
assert_eq!(visibility.len(), 0);
}
#[test]
fn test_event_log_default() {
let log = EventLog::<u64>::default();
assert_eq!(log.max_entries(), 100);
assert!(log.is_empty());
}
#[test]
fn test_log_entry_format_elapsed() {
let entry = LogEntry::<u64>::new(
"Test".into(),
EventType::Selection,
None,
);
let formatted = entry.format_elapsed();
assert!(formatted.ends_with("ago"));
}
}