use serde::{Deserialize, Serialize};
use crate::{Error, ObjectVersion, Result};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum UndoAction {
RemoveDeleteMarker {
marker_version_id: String,
revealed_version_id: String,
},
RestoreVersion {
source_version_id: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UndoPlanItem {
pub key: String,
pub expected_latest_version_id: String,
pub action: UndoAction,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct UndoPlan {
pub items: Vec<UndoPlanItem>,
}
impl UndoPlan {
pub fn new(mut items: Vec<UndoPlanItem>) -> Self {
items.sort_by(|left, right| left.key.cmp(&right.key));
Self { items }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum UndoOutcome {
Planned,
DeleteMarkerRemoved,
VersionRestored {
#[serde(skip_serializing_if = "Option::is_none")]
created_version_id: Option<String>,
},
Failed {
error_type: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
blocked_version_id: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UndoObjectResult {
pub key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub plan: Option<UndoPlanItem>,
pub outcome: UndoOutcome,
}
pub fn plan_object_undo(
key: &str,
history: &[ObjectVersion],
explicit_version_id: Option<&str>,
) -> Result<UndoPlanItem> {
if key.is_empty() {
return Err(Error::InvalidPath(
"Undo object key cannot be empty".to_string(),
));
}
if explicit_version_id.is_some_and(str::is_empty) {
return Err(Error::InvalidPath(
"Undo version ID cannot be empty".to_string(),
));
}
let mut versions = history
.iter()
.filter(|version| version.key == key)
.collect::<Vec<_>>();
if versions.is_empty() {
return Err(Error::NotFound(format!(
"No object version history found for {key}"
)));
}
let latest = exactly_one_latest(key, &versions)?.clone();
if let Some(version_id) = explicit_version_id {
let selected = versions
.iter()
.copied()
.find(|version| version.version_id == version_id)
.ok_or_else(|| Error::VersionNotFound {
path: key.to_string(),
version_id: version_id.to_string(),
})?;
if !selected.is_delete_marker {
if selected.is_latest {
return Err(Error::Conflict(format!(
"Version '{version_id}' is already current for {key}"
)));
}
return Ok(UndoPlanItem {
key: key.to_string(),
expected_latest_version_id: latest.version_id.clone(),
action: UndoAction::RestoreVersion {
source_version_id: version_id.to_string(),
},
});
}
if !selected.is_latest {
return Err(Error::Conflict(format!(
"Delete marker '{version_id}' is not current for {key}"
)));
}
}
versions.sort_by_key(|version| std::cmp::Reverse(version.last_modified));
ensure_unambiguous_order(key, &versions)?;
let current_index = versions
.iter()
.position(|version| version.is_latest)
.ok_or_else(|| Error::Conflict(format!("No current version found for {key}")))?;
if current_index != 0 {
return Err(Error::Conflict(format!(
"Version history ordering disagrees with the current version for {key}"
)));
}
let previous = versions.get(1).copied().ok_or_else(|| {
Error::Conflict(format!(
"Current version has no reversible predecessor for {key}"
))
})?;
let action = if latest.is_delete_marker {
if previous.is_delete_marker {
return Err(Error::Conflict(format!(
"Current delete marker is preceded by another delete marker for {key}"
)));
}
UndoAction::RemoveDeleteMarker {
marker_version_id: latest.version_id.clone(),
revealed_version_id: previous.version_id.clone(),
}
} else {
if previous.is_delete_marker {
return Err(Error::Conflict(format!(
"Current data version is preceded by a delete marker for {key}"
)));
}
UndoAction::RestoreVersion {
source_version_id: previous.version_id.clone(),
}
};
Ok(UndoPlanItem {
key: key.to_string(),
expected_latest_version_id: latest.version_id.clone(),
action,
})
}
fn exactly_one_latest<'a>(key: &str, versions: &'a [&ObjectVersion]) -> Result<&'a ObjectVersion> {
let mut latest = versions.iter().copied().filter(|version| version.is_latest);
let selected = latest
.next()
.ok_or_else(|| Error::Conflict(format!("No current version found for {key}")))?;
if latest.next().is_some() {
return Err(Error::Conflict(format!(
"Multiple current versions were reported for {key}"
)));
}
Ok(selected)
}
fn ensure_unambiguous_order(key: &str, versions: &[&ObjectVersion]) -> Result<()> {
let current = versions
.first()
.and_then(|version| version.last_modified)
.ok_or_else(|| {
Error::Conflict(format!(
"Version history is missing modification time for {key}"
))
})?;
let previous = versions
.get(1)
.and_then(|version| version.last_modified)
.ok_or_else(|| {
Error::Conflict(format!(
"Version history is missing a predecessor time for {key}"
))
})?;
if current == previous
|| versions
.get(2)
.and_then(|version| version.last_modified)
.is_some_and(|next| next == previous)
{
return Err(Error::Conflict(format!(
"Version history ordering is ambiguous for {key}"
)));
}
Ok(())
}