use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use uuid::Uuid;
pub type EventId = String;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum MetaEvent {
FileCreated { file: String },
FileDeleted { file: String },
FileMovedFrom { file: String, destination: String },
FileMovedTo { file: String, source: String },
PermissionChanged { file: String, mode: u32 },
DirectoryCreated,
DirectoryDeleted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetaLogEntry {
pub id: EventId,
pub timestamp: DateTime<Utc>,
pub event: MetaEvent,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
}
impl MetaLogEntry {
pub fn new(event: MetaEvent) -> Self {
MetaLogEntry {
id: format!("m-{}", Uuid::new_v4()),
timestamp: Utc::now(),
event,
author: None,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MetaLog {
pub directory: String,
pub events: Vec<MetaLogEntry>,
}
impl MetaLog {
pub fn new(directory: &str) -> Self {
MetaLog {
directory: directory.to_string(),
events: vec![],
}
}
pub fn record_event(&mut self, event: MetaEvent) -> EventId {
let entry = MetaLogEntry::new(event);
let event_id = entry.id.clone();
self.events.push(entry);
event_id
}
pub fn events_of_type(&self, event_type: &str) -> Vec<&MetaLogEntry> {
self.events
.iter()
.filter(|e| self.event_type_matches(&e.event, event_type))
.collect()
}
fn event_type_matches(&self, event: &MetaEvent, event_type: &str) -> bool {
matches!(
(event, event_type),
(MetaEvent::FileCreated { .. }, "FileCreated")
| (MetaEvent::FileDeleted { .. }, "FileDeleted")
| (MetaEvent::FileMovedFrom { .. }, "FileMovedFrom")
| (MetaEvent::FileMovedTo { .. }, "FileMovedTo")
| (MetaEvent::PermissionChanged { .. }, "PermissionChanged")
| (MetaEvent::DirectoryCreated, "DirectoryCreated")
| (MetaEvent::DirectoryDeleted, "DirectoryDeleted")
)
}
pub fn events_for_file(&self, filename: &str) -> Vec<&MetaLogEntry> {
self.events
.iter()
.filter(|e| self.event_involves_file(&e.event, filename))
.collect()
}
fn event_involves_file(&self, event: &MetaEvent, filename: &str) -> bool {
match event {
MetaEvent::FileCreated { file } => file == filename,
MetaEvent::FileDeleted { file } => file == filename,
MetaEvent::FileMovedFrom { file, .. } => file == filename,
MetaEvent::FileMovedTo { file, .. } => file == filename,
MetaEvent::PermissionChanged { file, .. } => file == filename,
_ => false,
}
}
pub fn save(&self, path: &Path) -> Result<()> {
let json = serde_json::to_string_pretty(self).context("Failed to serialize metalog")?;
fs::write(path, json).context(format!("Failed to write metalog to {:?}", path))?;
Ok(())
}
pub fn load(path: &Path) -> Result<Self> {
let json =
fs::read_to_string(path).context(format!("Failed to read metalog from {:?}", path))?;
let metalog: MetaLog =
serde_json::from_str(&json).context("Failed to deserialize metalog")?;
Ok(metalog)
}
pub fn get_or_create(directory: &Path) -> Result<Self> {
let metalog_path = Self::metalog_path(directory)?;
if metalog_path.exists() {
Self::load(&metalog_path)
} else {
Ok(Self::new(&directory.to_string_lossy()))
}
}
pub fn metalog_path(directory: &Path) -> Result<PathBuf> {
let wlk_dir = directory.join(".wlk");
Ok(wlk_dir.join("metalog"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_metalog() {
let metalog = MetaLog::new("src/");
assert_eq!(metalog.directory, "src/");
assert_eq!(metalog.events.len(), 0);
}
#[test]
fn test_record_event() {
let mut metalog = MetaLog::new("src/");
let id = metalog.record_event(MetaEvent::FileCreated {
file: "main.rs".to_string(),
});
assert_eq!(metalog.events.len(), 1);
assert_eq!(metalog.events[0].id, id);
assert!(matches!(
metalog.events[0].event,
MetaEvent::FileCreated { .. }
));
}
#[test]
fn test_file_lifecycle() {
let mut metalog = MetaLog::new("src/");
metalog.record_event(MetaEvent::FileCreated {
file: "temp.rs".to_string(),
});
metalog.record_event(MetaEvent::PermissionChanged {
file: "temp.rs".to_string(),
mode: 0o755,
});
metalog.record_event(MetaEvent::FileMovedFrom {
file: "temp.rs".to_string(),
destination: "../lib/temp.rs".to_string(),
});
assert_eq!(metalog.events.len(), 3);
let temp_events = metalog.events_for_file("temp.rs");
assert_eq!(temp_events.len(), 3);
}
#[test]
fn test_events_of_type() {
let mut metalog = MetaLog::new("src/");
metalog.record_event(MetaEvent::FileCreated {
file: "a.rs".to_string(),
});
metalog.record_event(MetaEvent::FileCreated {
file: "b.rs".to_string(),
});
metalog.record_event(MetaEvent::FileDeleted {
file: "c.rs".to_string(),
});
let created_events = metalog.events_of_type("FileCreated");
assert_eq!(created_events.len(), 2);
let deleted_events = metalog.events_of_type("FileDeleted");
assert_eq!(deleted_events.len(), 1);
}
#[test]
fn test_move_tracking() {
let mut src_metalog = MetaLog::new("src/");
let mut lib_metalog = MetaLog::new("lib/");
src_metalog.record_event(MetaEvent::FileMovedFrom {
file: "utils.rs".to_string(),
destination: "lib/utils.rs".to_string(),
});
lib_metalog.record_event(MetaEvent::FileMovedTo {
file: "utils.rs".to_string(),
source: "src/utils.rs".to_string(),
});
let src_events = src_metalog.events_for_file("utils.rs");
assert_eq!(src_events.len(), 1);
assert!(matches!(
src_events[0].event,
MetaEvent::FileMovedFrom { .. }
));
let lib_events = lib_metalog.events_for_file("utils.rs");
assert_eq!(lib_events.len(), 1);
assert!(matches!(lib_events[0].event, MetaEvent::FileMovedTo { .. }));
}
}