oximedia_workflow/
versioning.rs1#![allow(dead_code)]
31
32use std::collections::HashSet;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct WorkflowVersion {
41 pub id: u64,
43 pub data: String,
45}
46
47impl WorkflowVersion {
48 #[must_use]
50 pub fn new(id: u64, data: impl Into<String>) -> Self {
51 Self {
52 id,
53 data: data.into(),
54 }
55 }
56
57 #[must_use]
73 pub fn diff(&self, other: &WorkflowVersion) -> Vec<String> {
74 let self_lines: Vec<&str> = self.data.lines().collect();
75 let other_lines: Vec<&str> = other.data.lines().collect();
76
77 let self_set: HashSet<&str> = self_lines.iter().copied().collect();
78 let other_set: HashSet<&str> = other_lines.iter().copied().collect();
79
80 let mut result: Vec<String> = Vec::new();
81
82 for &line in &self_lines {
84 if !other_set.contains(line) {
85 result.push(format!("+{line}"));
86 }
87 }
88
89 for &line in &other_lines {
91 if !self_set.contains(line) {
92 result.push(format!("-{line}"));
93 }
94 }
95
96 result
97 }
98
99 #[must_use]
101 pub fn is_identical(&self, other: &WorkflowVersion) -> bool {
102 self.data == other.data
103 }
104
105 #[must_use]
107 pub fn summary(&self) -> VersionSummary {
108 let line_count = self.data.lines().count();
109 let byte_count = self.data.len();
110 VersionSummary {
111 id: self.id,
112 line_count,
113 byte_count,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct VersionSummary {
125 pub id: u64,
127 pub line_count: usize,
129 pub byte_count: usize,
131}
132
133#[derive(Debug, Default)]
139pub struct WorkflowVersionRegistry {
140 versions: Vec<WorkflowVersion>,
141}
142
143impl WorkflowVersionRegistry {
144 #[must_use]
146 pub fn new() -> Self {
147 Self::default()
148 }
149
150 pub fn push(&mut self, version: WorkflowVersion) {
152 self.versions.push(version);
153 }
154
155 #[must_use]
157 pub fn latest(&self) -> Option<&WorkflowVersion> {
158 self.versions.last()
159 }
160
161 #[must_use]
163 pub fn get(&self, id: u64) -> Option<&WorkflowVersion> {
164 self.versions.iter().find(|v| v.id == id)
165 }
166
167 #[must_use]
171 pub fn diff_ids(&self, from_id: u64, to_id: u64) -> Option<Vec<String>> {
172 let from = self.get(from_id)?;
173 let to = self.get(to_id)?;
174 Some(from.diff(to))
175 }
176
177 #[must_use]
179 pub fn len(&self) -> usize {
180 self.versions.len()
181 }
182
183 #[must_use]
185 pub fn is_empty(&self) -> bool {
186 self.versions.is_empty()
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_new_stores_id_and_data() {
196 let v = WorkflowVersion::new(42, "hello world");
197 assert_eq!(v.id, 42);
198 assert_eq!(v.data, "hello world");
199 }
200
201 #[test]
204 fn test_diff_identical_is_empty() {
205 let v1 = WorkflowVersion::new(1, "a\nb\nc");
206 let v2 = WorkflowVersion::new(2, "a\nb\nc");
207 assert!(v1.diff(&v2).is_empty());
208 }
209
210 #[test]
211 fn test_diff_added_lines() {
212 let v1 = WorkflowVersion::new(1, "a\nb\nc");
213 let v2 = WorkflowVersion::new(2, "a\nb");
214 let diff = v1.diff(&v2);
215 assert!(
217 diff.iter().any(|l| l == "+c"),
218 "Expected +c in diff: {diff:?}"
219 );
220 }
221
222 #[test]
223 fn test_diff_removed_lines() {
224 let v1 = WorkflowVersion::new(1, "a\nb");
225 let v2 = WorkflowVersion::new(2, "a\nb\nc");
226 let diff = v1.diff(&v2);
227 assert!(
229 diff.iter().any(|l| l == "-c"),
230 "Expected -c in diff: {diff:?}"
231 );
232 }
233
234 #[test]
235 fn test_diff_both_adds_and_removes() {
236 let v1 = WorkflowVersion::new(1, "step: ingest\nstep: transcode\n");
237 let v2 = WorkflowVersion::new(2, "step: ingest\nstep: upload\n");
238 let diff = v1.diff(&v2);
239 assert!(diff.iter().any(|l| l.starts_with('+')));
240 assert!(diff.iter().any(|l| l.starts_with('-')));
241 }
242
243 #[test]
244 fn test_is_identical_same_data() {
245 let v1 = WorkflowVersion::new(1, "data");
246 let v2 = WorkflowVersion::new(2, "data");
247 assert!(v1.is_identical(&v2));
248 }
249
250 #[test]
251 fn test_is_identical_different_data() {
252 let v1 = WorkflowVersion::new(1, "a");
253 let v2 = WorkflowVersion::new(2, "b");
254 assert!(!v1.is_identical(&v2));
255 }
256
257 #[test]
260 fn test_summary_line_count() {
261 let v = WorkflowVersion::new(1, "a\nb\nc");
262 let s = v.summary();
263 assert_eq!(s.id, 1);
264 assert_eq!(s.line_count, 3);
265 assert_eq!(s.byte_count, "a\nb\nc".len());
266 }
267
268 #[test]
271 fn test_registry_push_and_get() {
272 let mut reg = WorkflowVersionRegistry::new();
273 reg.push(WorkflowVersion::new(1, "v1"));
274 reg.push(WorkflowVersion::new(2, "v2"));
275 assert_eq!(reg.len(), 2);
276 assert_eq!(reg.get(1).map(|v| v.data.as_str()), Some("v1"));
277 assert_eq!(reg.get(2).map(|v| v.data.as_str()), Some("v2"));
278 }
279
280 #[test]
281 fn test_registry_latest() {
282 let mut reg = WorkflowVersionRegistry::new();
283 assert!(reg.latest().is_none());
284 reg.push(WorkflowVersion::new(1, "v1"));
285 reg.push(WorkflowVersion::new(2, "v2"));
286 assert_eq!(reg.latest().map(|v| v.id), Some(2));
287 }
288
289 #[test]
290 fn test_registry_diff_ids() {
291 let mut reg = WorkflowVersionRegistry::new();
292 reg.push(WorkflowVersion::new(1, "a\nb"));
293 reg.push(WorkflowVersion::new(2, "a\nc"));
294 let diff = reg.diff_ids(1, 2);
295 assert!(diff.is_some());
296 let diff = diff.unwrap();
297 assert!(!diff.is_empty());
298 }
299
300 #[test]
301 fn test_registry_diff_ids_missing_returns_none() {
302 let reg = WorkflowVersionRegistry::new();
303 assert!(reg.diff_ids(1, 2).is_none());
304 }
305
306 #[test]
307 fn test_registry_is_empty() {
308 let mut reg = WorkflowVersionRegistry::new();
309 assert!(reg.is_empty());
310 reg.push(WorkflowVersion::new(1, "x"));
311 assert!(!reg.is_empty());
312 }
313}