1#![allow(dead_code)]
8
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct WorkflowVersion {
18 pub major: u32,
20 pub minor: u32,
22 pub patch: u32,
24}
25
26impl WorkflowVersion {
27 #[must_use]
29 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
30 Self {
31 major,
32 minor,
33 patch,
34 }
35 }
36
37 #[must_use]
41 pub fn parse(s: &str) -> Option<Self> {
42 let parts: Vec<&str> = s.split('.').collect();
43 if parts.len() != 3 {
44 return None;
45 }
46 let major = parts[0].parse().ok()?;
47 let minor = parts[1].parse().ok()?;
48 let patch = parts[2].parse().ok()?;
49 Some(Self {
50 major,
51 minor,
52 patch,
53 })
54 }
55
56 #[must_use]
60 pub fn is_compatible_with(&self, other: &Self) -> bool {
61 self.major == other.major && other.minor >= self.minor
62 }
63
64 #[must_use]
66 pub fn to_string_repr(&self) -> String {
67 format!("{}.{}.{}", self.major, self.minor, self.patch)
68 }
69}
70
71impl std::fmt::Display for WorkflowVersion {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
74 }
75}
76
77#[derive(Debug, Clone)]
83pub struct VersionMigration {
84 pub from: WorkflowVersion,
86 pub to: WorkflowVersion,
88 pub description: String,
90}
91
92impl VersionMigration {
93 #[must_use]
95 pub fn new(from: WorkflowVersion, to: WorkflowVersion, description: impl Into<String>) -> Self {
96 Self {
97 from,
98 to,
99 description: description.into(),
100 }
101 }
102
103 #[must_use]
105 pub fn from_version(&self) -> &WorkflowVersion {
106 &self.from
107 }
108
109 #[must_use]
111 pub fn to_version(&self) -> &WorkflowVersion {
112 &self.to
113 }
114
115 #[must_use]
117 pub fn is_major_migration(&self) -> bool {
118 self.from.major != self.to.major
119 }
120}
121
122#[derive(Debug, Default)]
129pub struct WorkflowVersionRegistry {
130 versions: Vec<WorkflowVersion>,
131 migrations: HashMap<(WorkflowVersion, WorkflowVersion), VersionMigration>,
132}
133
134impl WorkflowVersionRegistry {
135 #[must_use]
137 pub fn new() -> Self {
138 Self::default()
139 }
140
141 pub fn register(&mut self, version: WorkflowVersion) {
143 if !self.versions.contains(&version) {
144 self.versions.push(version.clone());
145 self.versions.sort();
146 }
147 }
148
149 pub fn add_migration(&mut self, migration: VersionMigration) {
151 self.register(migration.from.clone());
152 self.register(migration.to.clone());
153 let key = (migration.from.clone(), migration.to.clone());
154 self.migrations.insert(key, migration);
155 }
156
157 #[must_use]
159 pub fn latest(&self) -> Option<&WorkflowVersion> {
160 self.versions.last()
161 }
162
163 #[must_use]
165 pub fn migrate(
166 &self,
167 from: &WorkflowVersion,
168 to: &WorkflowVersion,
169 ) -> Option<&VersionMigration> {
170 self.migrations.get(&(from.clone(), to.clone()))
171 }
172
173 #[must_use]
175 pub fn all_versions(&self) -> &[WorkflowVersion] {
176 &self.versions
177 }
178
179 #[must_use]
181 pub fn all_migrations(&self) -> Vec<&VersionMigration> {
182 self.migrations.values().collect()
183 }
184
185 #[must_use]
187 pub fn version_count(&self) -> usize {
188 self.versions.len()
189 }
190}
191
192#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_version_new_and_display() {
202 let v = WorkflowVersion::new(1, 2, 3);
203 assert_eq!(v.to_string(), "1.2.3");
204 }
205
206 #[test]
207 fn test_version_parse_valid() {
208 let v = WorkflowVersion::parse("2.0.1").expect("should succeed in test");
209 assert_eq!(v.major, 2);
210 assert_eq!(v.minor, 0);
211 assert_eq!(v.patch, 1);
212 }
213
214 #[test]
215 fn test_version_parse_invalid() {
216 assert!(WorkflowVersion::parse("1.2").is_none());
217 assert!(WorkflowVersion::parse("abc").is_none());
218 assert!(WorkflowVersion::parse("1.x.3").is_none());
219 }
220
221 #[test]
222 fn test_version_ordering() {
223 let v1 = WorkflowVersion::new(1, 0, 0);
224 let v2 = WorkflowVersion::new(1, 1, 0);
225 let v3 = WorkflowVersion::new(2, 0, 0);
226 assert!(v1 < v2);
227 assert!(v2 < v3);
228 }
229
230 #[test]
231 fn test_is_compatible_with_same_major() {
232 let v1 = WorkflowVersion::new(1, 2, 0);
233 let v2 = WorkflowVersion::new(1, 3, 0);
234 assert!(v1.is_compatible_with(&v2)); }
236
237 #[test]
238 fn test_is_compatible_with_older_minor() {
239 let v1 = WorkflowVersion::new(1, 3, 0);
240 let v2 = WorkflowVersion::new(1, 2, 0);
241 assert!(!v1.is_compatible_with(&v2)); }
243
244 #[test]
245 fn test_is_compatible_different_major() {
246 let v1 = WorkflowVersion::new(1, 0, 0);
247 let v2 = WorkflowVersion::new(2, 0, 0);
248 assert!(!v1.is_compatible_with(&v2));
249 }
250
251 #[test]
252 fn test_migration_from_version() {
253 let from = WorkflowVersion::new(1, 0, 0);
254 let to = WorkflowVersion::new(1, 1, 0);
255 let m = VersionMigration::new(from.clone(), to, "add field");
256 assert_eq!(m.from_version(), &from);
257 }
258
259 #[test]
260 fn test_migration_is_major() {
261 let m = VersionMigration::new(
262 WorkflowVersion::new(1, 0, 0),
263 WorkflowVersion::new(2, 0, 0),
264 "major bump",
265 );
266 assert!(m.is_major_migration());
267 }
268
269 #[test]
270 fn test_migration_not_major() {
271 let m = VersionMigration::new(
272 WorkflowVersion::new(1, 0, 0),
273 WorkflowVersion::new(1, 1, 0),
274 "minor bump",
275 );
276 assert!(!m.is_major_migration());
277 }
278
279 #[test]
280 fn test_registry_register_and_latest() {
281 let mut reg = WorkflowVersionRegistry::new();
282 reg.register(WorkflowVersion::new(1, 0, 0));
283 reg.register(WorkflowVersion::new(1, 1, 0));
284 reg.register(WorkflowVersion::new(2, 0, 0));
285 assert_eq!(
286 reg.latest().expect("should succeed in test"),
287 &WorkflowVersion::new(2, 0, 0)
288 );
289 }
290
291 #[test]
292 fn test_registry_add_migration_and_lookup() {
293 let mut reg = WorkflowVersionRegistry::new();
294 let from = WorkflowVersion::new(1, 0, 0);
295 let to = WorkflowVersion::new(1, 1, 0);
296 reg.add_migration(VersionMigration::new(from.clone(), to.clone(), "test"));
297 let m = reg.migrate(&from, &to).expect("should succeed in test");
298 assert_eq!(m.description, "test");
299 }
300
301 #[test]
302 fn test_registry_missing_migration() {
303 let reg = WorkflowVersionRegistry::new();
304 let v = WorkflowVersion::new(1, 0, 0);
305 assert!(reg.migrate(&v, &v).is_none());
306 }
307
308 #[test]
309 fn test_registry_version_count() {
310 let mut reg = WorkflowVersionRegistry::new();
311 reg.register(WorkflowVersion::new(1, 0, 0));
312 reg.register(WorkflowVersion::new(1, 0, 0)); assert_eq!(reg.version_count(), 1);
314 }
315
316 #[test]
317 fn test_registry_empty_latest() {
318 let reg = WorkflowVersionRegistry::new();
319 assert!(reg.latest().is_none());
320 }
321}