use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use uuid::Uuid;
pub type DeltaId = String;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum DeltaOperation {
Insert { index: usize, value: String },
Delete { index: usize, length: usize },
Replace {
index: usize,
length: usize,
value: String,
},
Snapshot { content: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Delta {
pub id: DeltaId,
pub parent: DeltaId,
pub timestamp: DateTime<Utc>,
pub operation: DeltaOperation,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
}
impl Delta {
pub fn new(parent: DeltaId, operation: DeltaOperation, annotation: Option<String>) -> Self {
Delta {
id: Self::generate_id(),
parent,
timestamp: Utc::now(),
operation,
annotation,
author: None,
}
}
pub fn initial_snapshot(content: String) -> Self {
Delta {
id: "d0".to_string(),
parent: "root".to_string(),
timestamp: Utc::now(),
operation: DeltaOperation::Snapshot { content },
annotation: Some("Initial snapshot".to_string()),
author: None,
}
}
fn generate_id() -> DeltaId {
format!("d-{}", Uuid::new_v4())
}
pub fn is_snapshot(&self) -> bool {
matches!(self.operation, DeltaOperation::Snapshot { .. })
}
}
impl fmt::Display for Delta {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let op_desc = match &self.operation {
DeltaOperation::Insert { index, value } => {
format!("Insert {} bytes at offset {}", value.len(), index)
}
DeltaOperation::Delete { index, length } => {
format!("Delete {} bytes at offset {}", length, index)
}
DeltaOperation::Replace {
index,
length,
value,
} => {
format!(
"Replace {} bytes at offset {} with {} bytes",
length,
index,
value.len()
)
}
DeltaOperation::Snapshot { content } => {
format!("Snapshot ({} bytes)", content.len())
}
};
write!(
f,
"[{}] {}: {}{}",
self.timestamp.format("%Y-%m-%d %H:%M:%S"),
self.id,
op_desc,
self.annotation
.as_ref()
.map(|a| format!(" - {}", a))
.unwrap_or_default()
)
}
}
pub fn apply_operation(content: &str, operation: &DeltaOperation) -> Result<String, String> {
match operation {
DeltaOperation::Insert { index, value } => {
if *index > content.len() {
return Err(format!(
"Insert index {} out of bounds (content length: {})",
index,
content.len()
));
}
let mut result = content.to_string();
result.insert_str(*index, value);
Ok(result)
}
DeltaOperation::Delete { index, length } => {
if *index + *length > content.len() {
return Err(format!(
"Delete range {}..{} out of bounds (content length: {})",
index,
index + length,
content.len()
));
}
let mut result = content.to_string();
result.replace_range(*index..*index + *length, "");
Ok(result)
}
DeltaOperation::Replace {
index,
length,
value,
} => {
if *index + *length > content.len() {
return Err(format!(
"Replace range {}..{} out of bounds (content length: {})",
index,
index + length,
content.len()
));
}
let mut result = content.to_string();
result.replace_range(*index..*index + *length, value);
Ok(result)
}
DeltaOperation::Snapshot { content } => Ok(content.clone()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_operation() {
let content = "Hello world";
let op = DeltaOperation::Insert {
index: 5,
value: " beautiful".to_string(),
};
let result = apply_operation(content, &op).unwrap();
assert_eq!(result, "Hello beautiful world");
}
#[test]
fn test_delete_operation() {
let content = "Hello beautiful world";
let op = DeltaOperation::Delete {
index: 5,
length: 10,
};
let result = apply_operation(content, &op).unwrap();
assert_eq!(result, "Hello world");
}
#[test]
fn test_replace_operation() {
let content = "Hello world";
let op = DeltaOperation::Replace {
index: 6,
length: 5,
value: "Rust".to_string(),
};
let result = apply_operation(content, &op).unwrap();
assert_eq!(result, "Hello Rust");
}
#[test]
fn test_snapshot_operation() {
let content = "old content";
let new_content = "completely new content";
let op = DeltaOperation::Snapshot {
content: new_content.to_string(),
};
let result = apply_operation(content, &op).unwrap();
assert_eq!(result, new_content);
}
#[test]
fn test_out_of_bounds_insert() {
let content = "Hello";
let op = DeltaOperation::Insert {
index: 10,
value: "!".to_string(),
};
assert!(apply_operation(content, &op).is_err());
}
#[test]
fn test_delta_display() {
let delta = Delta::new(
"d0".to_string(),
DeltaOperation::Insert {
index: 0,
value: "test".to_string(),
},
Some("Test annotation".to_string()),
);
let display = format!("{}", delta);
assert!(display.contains("Insert 4 bytes at offset 0"));
assert!(display.contains("Test annotation"));
}
}