1use serde::{Deserialize, Serialize};
4
5use crate::{Error, ObjectVersion, Result};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(tag = "kind", rename_all = "snake_case")]
10pub enum UndoAction {
11 RemoveDeleteMarker {
13 marker_version_id: String,
15 revealed_version_id: String,
17 },
18 RestoreVersion {
20 source_version_id: String,
22 },
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct UndoPlanItem {
28 pub key: String,
30 pub expected_latest_version_id: String,
32 pub action: UndoAction,
34}
35
36#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
38pub struct UndoPlan {
39 pub items: Vec<UndoPlanItem>,
41}
42
43impl UndoPlan {
44 pub fn new(mut items: Vec<UndoPlanItem>) -> Self {
46 items.sort_by(|left, right| left.key.cmp(&right.key));
47 Self { items }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(tag = "status", rename_all = "snake_case")]
54pub enum UndoOutcome {
55 Planned,
57 DeleteMarkerRemoved,
59 VersionRestored {
61 #[serde(skip_serializing_if = "Option::is_none")]
63 created_version_id: Option<String>,
64 },
65 Failed {
67 error_type: String,
69 message: String,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 blocked_version_id: Option<String>,
74 },
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct UndoObjectResult {
80 pub key: String,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub plan: Option<UndoPlanItem>,
85 pub outcome: UndoOutcome,
87}
88
89pub fn plan_object_undo(
95 key: &str,
96 history: &[ObjectVersion],
97 explicit_version_id: Option<&str>,
98) -> Result<UndoPlanItem> {
99 if key.is_empty() {
100 return Err(Error::InvalidPath(
101 "Undo object key cannot be empty".to_string(),
102 ));
103 }
104 if explicit_version_id.is_some_and(str::is_empty) {
105 return Err(Error::InvalidPath(
106 "Undo version ID cannot be empty".to_string(),
107 ));
108 }
109
110 let mut versions = history
111 .iter()
112 .filter(|version| version.key == key)
113 .collect::<Vec<_>>();
114 if versions.is_empty() {
115 return Err(Error::NotFound(format!(
116 "No object version history found for {key}"
117 )));
118 }
119
120 let latest = exactly_one_latest(key, &versions)?.clone();
121
122 if let Some(version_id) = explicit_version_id {
123 let selected = versions
124 .iter()
125 .copied()
126 .find(|version| version.version_id == version_id)
127 .ok_or_else(|| Error::VersionNotFound {
128 path: key.to_string(),
129 version_id: version_id.to_string(),
130 })?;
131
132 if !selected.is_delete_marker {
133 if selected.is_latest {
134 return Err(Error::Conflict(format!(
135 "Version '{version_id}' is already current for {key}"
136 )));
137 }
138 return Ok(UndoPlanItem {
139 key: key.to_string(),
140 expected_latest_version_id: latest.version_id.clone(),
141 action: UndoAction::RestoreVersion {
142 source_version_id: version_id.to_string(),
143 },
144 });
145 }
146 if !selected.is_latest {
147 return Err(Error::Conflict(format!(
148 "Delete marker '{version_id}' is not current for {key}"
149 )));
150 }
151 }
152
153 versions.sort_by_key(|version| std::cmp::Reverse(version.last_modified));
154 ensure_unambiguous_order(key, &versions)?;
155 let current_index = versions
156 .iter()
157 .position(|version| version.is_latest)
158 .ok_or_else(|| Error::Conflict(format!("No current version found for {key}")))?;
159 if current_index != 0 {
160 return Err(Error::Conflict(format!(
161 "Version history ordering disagrees with the current version for {key}"
162 )));
163 }
164 let previous = versions.get(1).copied().ok_or_else(|| {
165 Error::Conflict(format!(
166 "Current version has no reversible predecessor for {key}"
167 ))
168 })?;
169
170 let action = if latest.is_delete_marker {
171 if previous.is_delete_marker {
172 return Err(Error::Conflict(format!(
173 "Current delete marker is preceded by another delete marker for {key}"
174 )));
175 }
176 UndoAction::RemoveDeleteMarker {
177 marker_version_id: latest.version_id.clone(),
178 revealed_version_id: previous.version_id.clone(),
179 }
180 } else {
181 if previous.is_delete_marker {
182 return Err(Error::Conflict(format!(
183 "Current data version is preceded by a delete marker for {key}"
184 )));
185 }
186 UndoAction::RestoreVersion {
187 source_version_id: previous.version_id.clone(),
188 }
189 };
190
191 Ok(UndoPlanItem {
192 key: key.to_string(),
193 expected_latest_version_id: latest.version_id.clone(),
194 action,
195 })
196}
197
198fn exactly_one_latest<'a>(key: &str, versions: &'a [&ObjectVersion]) -> Result<&'a ObjectVersion> {
199 let mut latest = versions.iter().copied().filter(|version| version.is_latest);
200 let selected = latest
201 .next()
202 .ok_or_else(|| Error::Conflict(format!("No current version found for {key}")))?;
203 if latest.next().is_some() {
204 return Err(Error::Conflict(format!(
205 "Multiple current versions were reported for {key}"
206 )));
207 }
208 Ok(selected)
209}
210
211fn ensure_unambiguous_order(key: &str, versions: &[&ObjectVersion]) -> Result<()> {
212 let current = versions
213 .first()
214 .and_then(|version| version.last_modified)
215 .ok_or_else(|| {
216 Error::Conflict(format!(
217 "Version history is missing modification time for {key}"
218 ))
219 })?;
220 let previous = versions
221 .get(1)
222 .and_then(|version| version.last_modified)
223 .ok_or_else(|| {
224 Error::Conflict(format!(
225 "Version history is missing a predecessor time for {key}"
226 ))
227 })?;
228 if current == previous
229 || versions
230 .get(2)
231 .and_then(|version| version.last_modified)
232 .is_some_and(|next| next == previous)
233 {
234 return Err(Error::Conflict(format!(
235 "Version history ordering is ambiguous for {key}"
236 )));
237 }
238 Ok(())
239}