1use anyhow::{Result, bail};
2
3pub type MigrateFn = fn(serde_json::Value) -> Result<serde_json::Value>;
6
7pub fn migrate(
16 mut value: serde_json::Value,
17 from: u32,
18 to: u32,
19 migrations: &[MigrateFn],
20) -> Result<serde_json::Value> {
21 if from > to {
22 bail!(
23 "cannot downgrade schema from version {} to {} (downgrade not supported)",
24 from,
25 to
26 );
27 }
28 for version in from..to {
29 let idx = version as usize;
30 if idx >= migrations.len() {
31 bail!(
32 "no migration available from version {} to {} (only {} migration(s) defined)",
33 version,
34 version + 1,
35 migrations.len()
36 );
37 }
38 value = migrations[idx](value)?;
39 }
40 Ok(value)
41}
42
43pub fn schema_version_of(value: &serde_json::Value) -> u32 {
46 value
47 .get("schema_version")
48 .and_then(|v| v.as_u64())
49 .map(|v| v as u32)
50 .unwrap_or(0)
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use serde_json::json;
57
58 fn bump_version(mut v: serde_json::Value) -> Result<serde_json::Value> {
59 v["schema_version"] = json!(
60 v.get("schema_version")
61 .and_then(|s| s.as_u64())
62 .unwrap_or(0)
63 + 1
64 );
65 Ok(v)
66 }
67
68 fn add_field(mut v: serde_json::Value) -> Result<serde_json::Value> {
69 v["new_field"] = json!("added");
70 Ok(v)
71 }
72
73 #[test]
74 fn migrate_noop() {
75 let value = json!({"schema_version": 1, "mode": "dev"});
76 let result = migrate(value.clone(), 1, 1, &[]).unwrap();
77 assert_eq!(result, value);
78 }
79
80 #[test]
81 fn migrate_one_step() {
82 let value = json!({"schema_version": 0, "mode": "dev"});
83 let result = migrate(value, 0, 1, &[bump_version as MigrateFn]).unwrap();
84 assert_eq!(result["schema_version"], 1);
85 assert_eq!(result["mode"], "dev");
86 }
87
88 #[test]
89 fn migrate_chain() {
90 let value = json!({"schema_version": 0, "mode": "flake"});
91 let result = migrate(
92 value,
93 0,
94 2,
95 &[bump_version as MigrateFn, add_field as MigrateFn],
96 )
97 .unwrap();
98 assert_eq!(result["schema_version"], 1); assert_eq!(result["new_field"], "added");
100 assert_eq!(result["mode"], "flake");
101 }
102
103 #[test]
104 fn migrate_downgrade_err() {
105 let value = json!({"schema_version": 2});
106 let err = migrate(value, 2, 1, &[]).unwrap_err();
107 assert!(err.to_string().contains("downgrade not supported"));
108 }
109
110 #[test]
111 fn migrate_missing_migration_err() {
112 let value = json!({"schema_version": 0});
113 let err = migrate(value, 0, 2, &[bump_version as MigrateFn]).unwrap_err();
115 assert!(err.to_string().contains("no migration available"));
116 }
117
118 #[test]
119 fn schema_version_of_present() {
120 let v = json!({"schema_version": 3, "mode": "dev"});
121 assert_eq!(schema_version_of(&v), 3);
122 }
123
124 #[test]
125 fn schema_version_of_missing() {
126 let v = json!({"mode": "dev"});
127 assert_eq!(schema_version_of(&v), 0);
128 }
129
130 #[test]
131 fn migrate_run_info_from_unversioned() {
132 let old_json = json!({"mode": "dev", "guest_user": "ubuntu"});
136 let from = schema_version_of(&old_json); let result = migrate(old_json.clone(), from, 0, &[]).unwrap();
138 assert_eq!(result["mode"], "dev");
140 assert_eq!(result["guest_user"], "ubuntu");
141 }
142}