use serde::{Deserialize, Serialize};
use std::fmt;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FileOperation {
Append,
Prepend,
Insert,
Delete,
Replace,
Create,
Remove,
Relocate,
Rename,
Chmod,
Read,
}
impl FileOperation {
pub fn code(&self) -> char {
match self {
FileOperation::Append => 'A',
FileOperation::Prepend => 'P',
FileOperation::Insert => 'I',
FileOperation::Delete => 'D',
FileOperation::Replace => 'R',
FileOperation::Create => 'C',
FileOperation::Remove => 'X',
FileOperation::Relocate => 'M',
FileOperation::Rename => 'N',
FileOperation::Chmod => 'H',
FileOperation::Read => 'r',
}
}
pub fn from_code(code: char) -> Option<Self> {
match code {
'A' => Some(FileOperation::Append),
'P' => Some(FileOperation::Prepend),
'I' => Some(FileOperation::Insert),
'D' => Some(FileOperation::Delete),
'R' => Some(FileOperation::Replace),
'C' => Some(FileOperation::Create),
'X' => Some(FileOperation::Remove),
'M' => Some(FileOperation::Relocate),
'N' => Some(FileOperation::Rename),
'H' => Some(FileOperation::Chmod),
'r' => Some(FileOperation::Read),
_ => None,
}
}
pub fn is_safe(&self) -> bool {
matches!(self, FileOperation::Append | FileOperation::Read)
}
pub fn description(&self) -> &'static str {
match self {
FileOperation::Append => "Appended content to file",
FileOperation::Prepend => "Prepended content to file",
FileOperation::Insert => "Inserted content into file",
FileOperation::Delete => "Deleted content from file",
FileOperation::Replace => "Replaced content in file",
FileOperation::Create => "Created new file",
FileOperation::Remove => "Removed file",
FileOperation::Relocate => "Relocated file",
FileOperation::Rename => "Renamed file",
FileOperation::Chmod => "Changed file permissions",
FileOperation::Read => "Read file",
}
}
}
impl fmt::Display for FileOperation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.description())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationContext {
pub operation: FileOperation,
pub position: Option<usize>,
pub bytes_affected: usize,
pub old_hash: Option<String>,
pub new_hash: Option<String>,
pub metadata: Option<serde_json::Value>,
}
impl OperationContext {
pub fn new(operation: FileOperation) -> Self {
Self {
operation,
position: None,
bytes_affected: 0,
old_hash: None,
new_hash: None,
metadata: None,
}
}
pub fn with_position(mut self, pos: usize) -> Self {
self.position = Some(pos);
self
}
pub fn with_bytes(mut self, bytes: usize) -> Self {
self.bytes_affected = bytes;
self
}
pub fn with_hashes(mut self, old: Option<String>, new: Option<String>) -> Self {
self.old_hash = old;
self.new_hash = new;
self
}
}
pub fn suggest_operation(
original: Option<&str>,
modified: &str,
prefer_append: bool,
) -> FileOperation {
match original {
None => FileOperation::Create,
Some("") => FileOperation::Append,
Some(orig) => {
if prefer_append && modified.starts_with(orig) {
FileOperation::Append
}
else if modified.ends_with(orig) {
FileOperation::Prepend
}
else {
FileOperation::Replace
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_operation_codes() {
assert_eq!(FileOperation::Append.code(), 'A');
assert_eq!(FileOperation::from_code('A'), Some(FileOperation::Append));
}
#[test]
fn test_suggest_operation() {
let op = suggest_operation(Some("hello"), "hello world", true);
assert_eq!(op, FileOperation::Append);
let op = suggest_operation(Some("world"), "hello world", false);
assert_eq!(op, FileOperation::Prepend);
let op = suggest_operation(None, "new content", true);
assert_eq!(op, FileOperation::Create);
}
}