Skip to main content

oxihuman_core/
version_migration.rs

1//! Version migration utilities for assets and schemas.
2
3#[allow(dead_code)]
4#[derive(Clone, PartialEq, Debug)]
5pub struct SemVer {
6    pub major: u32,
7    pub minor: u32,
8    pub patch: u32,
9}
10
11#[allow(dead_code)]
12#[derive(Clone)]
13pub struct MigrationStep {
14    pub from_version: SemVer,
15    pub to_version: SemVer,
16    pub description: String,
17    pub breaking: bool,
18}
19
20#[allow(dead_code)]
21pub struct MigrationPlan {
22    pub steps: Vec<MigrationStep>,
23    pub source: SemVer,
24    pub target: SemVer,
25}
26
27#[allow(dead_code)]
28pub struct MigrationRegistry {
29    pub steps: Vec<MigrationStep>,
30}
31
32#[allow(dead_code)]
33pub fn new_semver(major: u32, minor: u32, patch: u32) -> SemVer {
34    SemVer {
35        major,
36        minor,
37        patch,
38    }
39}
40
41#[allow(dead_code)]
42pub fn semver_parse(s: &str) -> Option<SemVer> {
43    let parts: Vec<&str> = s.splitn(3, '.').collect();
44    if parts.len() != 3 {
45        return None;
46    }
47    let major = parts[0].parse::<u32>().ok()?;
48    let minor = parts[1].parse::<u32>().ok()?;
49    let patch = parts[2].parse::<u32>().ok()?;
50    Some(SemVer {
51        major,
52        minor,
53        patch,
54    })
55}
56
57#[allow(dead_code)]
58pub fn semver_compare(a: &SemVer, b: &SemVer) -> std::cmp::Ordering {
59    a.major
60        .cmp(&b.major)
61        .then(a.minor.cmp(&b.minor))
62        .then(a.patch.cmp(&b.patch))
63}
64
65#[allow(dead_code)]
66pub fn semver_to_string(v: &SemVer) -> String {
67    format!("{}.{}.{}", v.major, v.minor, v.patch)
68}
69
70#[allow(dead_code)]
71pub fn is_breaking_change(from: &SemVer, to: &SemVer) -> bool {
72    to.major > from.major
73}
74
75#[allow(dead_code)]
76pub fn new_migration_registry() -> MigrationRegistry {
77    MigrationRegistry { steps: Vec::new() }
78}
79
80#[allow(dead_code)]
81pub fn register_migration(registry: &mut MigrationRegistry, step: MigrationStep) {
82    registry.steps.push(step);
83}
84
85/// Find a chain of steps from `from` to `to` using BFS.
86#[allow(dead_code)]
87pub fn plan_migration(
88    registry: &MigrationRegistry,
89    from: &SemVer,
90    to: &SemVer,
91) -> Option<MigrationPlan> {
92    if from == to {
93        return Some(MigrationPlan {
94            steps: Vec::new(),
95            source: from.clone(),
96            target: to.clone(),
97        });
98    }
99
100    // BFS over version graph
101    use std::collections::{HashMap, VecDeque};
102
103    let mut queue: VecDeque<SemVer> = VecDeque::new();
104    // predecessor: current -> (predecessor, step_index)
105    let mut prev: HashMap<String, (SemVer, usize)> = HashMap::new();
106
107    queue.push_back(from.clone());
108
109    while let Some(current) = queue.pop_front() {
110        for (idx, step) in registry.steps.iter().enumerate() {
111            if step.from_version == current {
112                let next = step.to_version.clone();
113                let next_key = semver_to_string(&next);
114                if !prev.contains_key(&next_key)
115                    && semver_to_string(&next) != semver_to_string(from)
116                {
117                    prev.insert(next_key.clone(), (current.clone(), idx));
118                    if &next == to {
119                        // Reconstruct path
120                        let mut path_steps: Vec<MigrationStep> = Vec::new();
121                        let mut cur = next;
122                        loop {
123                            let cur_key = semver_to_string(&cur);
124                            if let Some((pred, sidx)) = prev.get(&cur_key) {
125                                path_steps.push(registry.steps[*sidx].clone());
126                                if pred == from {
127                                    break;
128                                }
129                                cur = pred.clone();
130                            } else {
131                                break;
132                            }
133                        }
134                        path_steps.reverse();
135                        return Some(MigrationPlan {
136                            steps: path_steps,
137                            source: from.clone(),
138                            target: to.clone(),
139                        });
140                    }
141                    queue.push_back(next);
142                }
143            }
144        }
145    }
146    None
147}
148
149#[allow(dead_code)]
150pub fn has_migration_path(registry: &MigrationRegistry, from: &SemVer, to: &SemVer) -> bool {
151    plan_migration(registry, from, to).is_some()
152}
153
154#[allow(dead_code)]
155pub fn migration_step_count(plan: &MigrationPlan) -> usize {
156    plan.steps.len()
157}
158
159#[allow(dead_code)]
160pub fn plan_has_breaking(plan: &MigrationPlan) -> bool {
161    plan.steps.iter().any(|s| s.breaking)
162}
163
164#[allow(dead_code)]
165pub fn migration_description(plan: &MigrationPlan) -> Vec<&str> {
166    plan.steps.iter().map(|s| s.description.as_str()).collect()
167}
168
169#[allow(dead_code)]
170pub fn latest_version(registry: &MigrationRegistry) -> Option<SemVer> {
171    registry
172        .steps
173        .iter()
174        .map(|s| &s.to_version)
175        .max_by(|a, b| semver_compare(a, b))
176        .cloned()
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    fn v(major: u32, minor: u32, patch: u32) -> SemVer {
184        new_semver(major, minor, patch)
185    }
186
187    fn make_step(from: SemVer, to: SemVer, desc: &str, breaking: bool) -> MigrationStep {
188        MigrationStep {
189            from_version: from,
190            to_version: to,
191            description: desc.to_string(),
192            breaking,
193        }
194    }
195
196    #[test]
197    fn test_semver_parse_valid() {
198        let sv = semver_parse("1.2.3").expect("should succeed");
199        assert_eq!(sv.major, 1);
200        assert_eq!(sv.minor, 2);
201        assert_eq!(sv.patch, 3);
202    }
203
204    #[test]
205    fn test_semver_parse_invalid() {
206        assert!(semver_parse("1.2").is_none());
207        assert!(semver_parse("abc").is_none());
208        assert!(semver_parse("1.x.3").is_none());
209    }
210
211    #[test]
212    fn test_semver_compare_less() {
213        assert_eq!(
214            semver_compare(&v(1, 0, 0), &v(2, 0, 0)),
215            std::cmp::Ordering::Less
216        );
217        assert_eq!(
218            semver_compare(&v(1, 2, 3), &v(1, 2, 4)),
219            std::cmp::Ordering::Less
220        );
221        assert_eq!(
222            semver_compare(&v(1, 0, 0), &v(1, 1, 0)),
223            std::cmp::Ordering::Less
224        );
225    }
226
227    #[test]
228    fn test_semver_compare_equal() {
229        assert_eq!(
230            semver_compare(&v(1, 2, 3), &v(1, 2, 3)),
231            std::cmp::Ordering::Equal
232        );
233    }
234
235    #[test]
236    fn test_semver_compare_greater() {
237        assert_eq!(
238            semver_compare(&v(2, 0, 0), &v(1, 9, 9)),
239            std::cmp::Ordering::Greater
240        );
241    }
242
243    #[test]
244    fn test_semver_to_string() {
245        assert_eq!(semver_to_string(&v(3, 14, 0)), "3.14.0");
246    }
247
248    #[test]
249    fn test_is_breaking_change_true() {
250        assert!(is_breaking_change(&v(1, 5, 0), &v(2, 0, 0)));
251    }
252
253    #[test]
254    fn test_is_breaking_change_false_minor_bump() {
255        assert!(!is_breaking_change(&v(1, 0, 0), &v(1, 1, 0)));
256    }
257
258    #[test]
259    fn test_is_breaking_change_false_patch_bump() {
260        assert!(!is_breaking_change(&v(1, 0, 0), &v(1, 0, 1)));
261    }
262
263    #[test]
264    fn test_register_migration() {
265        let mut registry = new_migration_registry();
266        register_migration(
267            &mut registry,
268            make_step(v(1, 0, 0), v(1, 1, 0), "add field", false),
269        );
270        assert_eq!(registry.steps.len(), 1);
271    }
272
273    #[test]
274    fn test_plan_migration_single_step() {
275        let mut registry = new_migration_registry();
276        register_migration(
277            &mut registry,
278            make_step(v(1, 0, 0), v(1, 1, 0), "bump minor", false),
279        );
280        let plan = plan_migration(&registry, &v(1, 0, 0), &v(1, 1, 0)).expect("should succeed");
281        assert_eq!(migration_step_count(&plan), 1);
282    }
283
284    #[test]
285    fn test_plan_migration_multi_step() {
286        let mut registry = new_migration_registry();
287        register_migration(
288            &mut registry,
289            make_step(v(1, 0, 0), v(1, 1, 0), "step1", false),
290        );
291        register_migration(
292            &mut registry,
293            make_step(v(1, 1, 0), v(2, 0, 0), "step2", true),
294        );
295        let plan = plan_migration(&registry, &v(1, 0, 0), &v(2, 0, 0)).expect("should succeed");
296        assert_eq!(migration_step_count(&plan), 2);
297    }
298
299    #[test]
300    fn test_plan_migration_no_path() {
301        let mut registry = new_migration_registry();
302        register_migration(
303            &mut registry,
304            make_step(v(1, 0, 0), v(1, 1, 0), "step1", false),
305        );
306        let result = plan_migration(&registry, &v(1, 0, 0), &v(3, 0, 0));
307        assert!(result.is_none());
308    }
309
310    #[test]
311    fn test_has_migration_path_true() {
312        let mut registry = new_migration_registry();
313        register_migration(&mut registry, make_step(v(1, 0, 0), v(1, 1, 0), "s", false));
314        assert!(has_migration_path(&registry, &v(1, 0, 0), &v(1, 1, 0)));
315    }
316
317    #[test]
318    fn test_has_migration_path_false() {
319        let registry = new_migration_registry();
320        assert!(!has_migration_path(&registry, &v(1, 0, 0), &v(2, 0, 0)));
321    }
322
323    #[test]
324    fn test_migration_step_count() {
325        let mut registry = new_migration_registry();
326        register_migration(
327            &mut registry,
328            make_step(v(1, 0, 0), v(1, 1, 0), "s1", false),
329        );
330        register_migration(
331            &mut registry,
332            make_step(v(1, 1, 0), v(1, 2, 0), "s2", false),
333        );
334        let plan = plan_migration(&registry, &v(1, 0, 0), &v(1, 2, 0)).expect("should succeed");
335        assert_eq!(migration_step_count(&plan), 2);
336    }
337
338    #[test]
339    fn test_plan_has_breaking_true() {
340        let mut registry = new_migration_registry();
341        register_migration(
342            &mut registry,
343            make_step(v(1, 0, 0), v(2, 0, 0), "major", true),
344        );
345        let plan = plan_migration(&registry, &v(1, 0, 0), &v(2, 0, 0)).expect("should succeed");
346        assert!(plan_has_breaking(&plan));
347    }
348
349    #[test]
350    fn test_plan_has_breaking_false() {
351        let mut registry = new_migration_registry();
352        register_migration(
353            &mut registry,
354            make_step(v(1, 0, 0), v(1, 1, 0), "minor", false),
355        );
356        let plan = plan_migration(&registry, &v(1, 0, 0), &v(1, 1, 0)).expect("should succeed");
357        assert!(!plan_has_breaking(&plan));
358    }
359
360    #[test]
361    fn test_latest_version() {
362        let mut registry = new_migration_registry();
363        register_migration(
364            &mut registry,
365            make_step(v(1, 0, 0), v(1, 1, 0), "s1", false),
366        );
367        register_migration(&mut registry, make_step(v(1, 1, 0), v(2, 0, 0), "s2", true));
368        let latest = latest_version(&registry).expect("should succeed");
369        assert_eq!(latest, v(2, 0, 0));
370    }
371
372    #[test]
373    fn test_latest_version_none_on_empty() {
374        let registry = new_migration_registry();
375        assert!(latest_version(&registry).is_none());
376    }
377
378    #[test]
379    fn test_migration_description() {
380        let mut registry = new_migration_registry();
381        register_migration(
382            &mut registry,
383            make_step(v(1, 0, 0), v(1, 1, 0), "add feature", false),
384        );
385        let plan = plan_migration(&registry, &v(1, 0, 0), &v(1, 1, 0)).expect("should succeed");
386        let desc = migration_description(&plan);
387        assert_eq!(desc, vec!["add feature"]);
388    }
389
390    #[test]
391    fn test_new_semver() {
392        let sv = new_semver(2, 3, 4);
393        assert_eq!(sv.major, 2);
394        assert_eq!(sv.minor, 3);
395        assert_eq!(sv.patch, 4);
396    }
397}