Skip to main content

amaters_server/
migration.rs

1//! Versioned document migration framework.
2//!
3//! Provides a registry of version-to-version migrations and computes
4//! the shortest migration path between any two reachable versions using BFS.
5
6use crate::server::{ServerError, ServerResult};
7use serde_json::Value;
8use std::collections::{HashMap, HashSet, VecDeque};
9
10/// A mutable document context passed through each migration step.
11pub struct MigrationContext {
12    doc: Value,
13}
14
15impl MigrationContext {
16    /// Create a new migration context from a document value.
17    pub fn new(doc: Value) -> Self {
18        Self { doc }
19    }
20
21    /// Get a field from the document by key.
22    pub fn get(&self, key: &str) -> Option<&Value> {
23        self.doc.get(key)
24    }
25
26    /// Set a field in the document.
27    pub fn set(&mut self, key: &str, value: Value) {
28        if let Value::Object(map) = &mut self.doc {
29            map.insert(key.to_owned(), value);
30        }
31    }
32
33    /// Remove a field from the document, returning its previous value if present.
34    pub fn remove(&mut self, key: &str) -> Option<Value> {
35        if let Value::Object(map) = &mut self.doc {
36            map.remove(key)
37        } else {
38            None
39        }
40    }
41
42    /// Consume the context and return the underlying document.
43    pub fn into_doc(self) -> Value {
44        self.doc
45    }
46
47    /// Borrow the underlying document value.
48    pub fn doc(&self) -> &Value {
49        &self.doc
50    }
51}
52
53/// A single version-to-version migration step.
54pub trait Migration: Send + Sync {
55    /// The version this migration migrates FROM.
56    #[allow(clippy::wrong_self_convention)]
57    fn from_version(&self) -> (u64, u64, u64);
58
59    /// The version this migration migrates TO.
60    fn to_version(&self) -> (u64, u64, u64);
61
62    /// Human-readable description of what this migration does.
63    fn description(&self) -> &str;
64
65    /// Apply the migration to a document context.
66    fn migrate(&self, ctx: &mut MigrationContext) -> ServerResult<()>;
67}
68
69/// An ordered sequence of migration steps computed by [`MigrationRegistry::plan`].
70///
71/// Holds a reference to the registry's migration list and the indices of the
72/// steps to apply, avoiding any copies of the migration objects.
73pub struct MigrationPlan<'a> {
74    migrations: &'a [Box<dyn Migration>],
75    indices: Vec<usize>,
76}
77
78impl<'a> std::fmt::Debug for MigrationPlan<'a> {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.debug_struct("MigrationPlan")
81            .field("steps", &self.indices.len())
82            .finish()
83    }
84}
85
86impl<'a> MigrationPlan<'a> {
87    fn new(migrations: &'a [Box<dyn Migration>], indices: Vec<usize>) -> Self {
88        Self {
89            migrations,
90            indices,
91        }
92    }
93
94    /// Returns true if no migration steps are needed (source == target version).
95    pub fn is_empty(&self) -> bool {
96        self.indices.is_empty()
97    }
98
99    /// Number of migration steps in this plan.
100    pub fn len(&self) -> usize {
101        self.indices.len()
102    }
103
104    /// Apply all migration steps in order to the given context.
105    ///
106    /// Stops and returns the first error encountered.
107    pub fn apply(&self, ctx: &mut MigrationContext) -> ServerResult<()> {
108        for &idx in &self.indices {
109            self.migrations[idx].migrate(ctx)?;
110        }
111        Ok(())
112    }
113
114    /// Return descriptions of each step in this plan for logging/audit.
115    pub fn step_descriptions(&self) -> Vec<&str> {
116        self.indices
117            .iter()
118            .map(|&idx| self.migrations[idx].description())
119            .collect()
120    }
121}
122
123/// Registry of all known migrations.
124///
125/// Use [`MigrationRegistry::register`] to add migrations, then
126/// [`MigrationRegistry::plan`] to compute the shortest path between versions.
127pub struct MigrationRegistry {
128    migrations: Vec<Box<dyn Migration>>,
129}
130
131impl MigrationRegistry {
132    /// Create a new empty registry.
133    pub fn new() -> Self {
134        Self {
135            migrations: Vec::new(),
136        }
137    }
138
139    /// Register a migration. Returns `&mut Self` for chaining.
140    pub fn register(&mut self, m: impl Migration + 'static) -> &mut Self {
141        self.migrations.push(Box::new(m));
142        self
143    }
144
145    /// Compute the shortest migration path from `from` to `to` using BFS.
146    ///
147    /// Returns an empty plan if `from == to`.
148    /// Returns `Err(ServerError::Migration(...))` if no path exists.
149    ///
150    /// When multiple paths of equal length exist, BFS naturally picks one;
151    /// the specific choice among equal-length paths is not guaranteed.
152    pub fn plan(
153        &self,
154        from: (u64, u64, u64),
155        to: (u64, u64, u64),
156    ) -> ServerResult<MigrationPlan<'_>> {
157        if from == to {
158            return Ok(MigrationPlan::new(&self.migrations, vec![]));
159        }
160
161        // Build adjacency list: from_version → Vec<migration_index>
162        let mut adj: HashMap<(u64, u64, u64), Vec<usize>> = HashMap::new();
163        for (idx, m) in self.migrations.iter().enumerate() {
164            adj.entry(m.from_version()).or_default().push(idx);
165        }
166
167        // BFS: each queue entry is (current_version, path_of_indices_so_far)
168        let mut queue: VecDeque<((u64, u64, u64), Vec<usize>)> = VecDeque::new();
169        let mut visited: HashSet<(u64, u64, u64)> = HashSet::new();
170        queue.push_back((from, vec![]));
171        visited.insert(from);
172
173        while let Some((cur, path)) = queue.pop_front() {
174            if let Some(neighbors) = adj.get(&cur) {
175                for &idx in neighbors {
176                    let next = self.migrations[idx].to_version();
177                    let mut new_path = path.clone();
178                    new_path.push(idx);
179
180                    if next == to {
181                        return Ok(MigrationPlan::new(&self.migrations, new_path));
182                    }
183
184                    if !visited.contains(&next) {
185                        visited.insert(next);
186                        queue.push_back((next, new_path));
187                    }
188                }
189            }
190        }
191
192        Err(ServerError::Migration(format!(
193            "No migration path from {from:?} to {to:?}"
194        )))
195    }
196}
197
198impl Default for MigrationRegistry {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use serde_json::json;
208
209    /// A simple migration that sets a single field to a fixed value.
210    struct SetFieldMigration {
211        from: (u64, u64, u64),
212        to: (u64, u64, u64),
213        desc: String,
214        field: String,
215        value: Value,
216    }
217
218    impl SetFieldMigration {
219        fn new(from: (u64, u64, u64), to: (u64, u64, u64), field: &str, value: Value) -> Self {
220            let desc = format!("{from:?} -> {to:?}: set {field}");
221            Self {
222                from,
223                to,
224                desc,
225                field: field.to_owned(),
226                value,
227            }
228        }
229    }
230
231    impl Migration for SetFieldMigration {
232        fn from_version(&self) -> (u64, u64, u64) {
233            self.from
234        }
235
236        fn to_version(&self) -> (u64, u64, u64) {
237            self.to
238        }
239
240        fn description(&self) -> &str {
241            &self.desc
242        }
243
244        fn migrate(&self, ctx: &mut MigrationContext) -> ServerResult<()> {
245            ctx.set(&self.field, self.value.clone());
246            Ok(())
247        }
248    }
249
250    #[test]
251    fn test_linear_path() {
252        let mut registry = MigrationRegistry::new();
253        registry
254            .register(SetFieldMigration::new(
255                (0, 1, 0),
256                (0, 2, 0),
257                "step1",
258                json!("applied"),
259            ))
260            .register(SetFieldMigration::new(
261                (0, 2, 0),
262                (0, 3, 0),
263                "step2",
264                json!("applied"),
265            ));
266
267        let plan = registry
268            .plan((0, 1, 0), (0, 3, 0))
269            .expect("plan should exist");
270        assert_eq!(plan.len(), 2);
271
272        let mut ctx = MigrationContext::new(json!({}));
273        plan.apply(&mut ctx).expect("apply should succeed");
274
275        let doc = ctx.into_doc();
276        assert_eq!(doc.get("step1"), Some(&json!("applied")));
277        assert_eq!(doc.get("step2"), Some(&json!("applied")));
278    }
279
280    #[test]
281    fn test_already_at_target_returns_empty_plan() {
282        let registry = MigrationRegistry::new();
283        let plan = registry
284            .plan((0, 2, 0), (0, 2, 0))
285            .expect("same-version plan");
286        assert!(plan.is_empty());
287        assert_eq!(plan.len(), 0);
288    }
289
290    #[test]
291    fn test_no_path_returns_migration_error() {
292        let registry = MigrationRegistry::new(); // empty — no migrations
293        let result = registry.plan((0, 1, 0), (0, 9, 0));
294        assert!(
295            matches!(result, Err(ServerError::Migration(_))),
296            "expected Migration error, got: {result:?}"
297        );
298    }
299
300    #[test]
301    fn test_branch_picks_shortest_path() {
302        // Graph:
303        //   (0,1,0) → (0,2,0) → (0,3,0)  [2 hops]
304        //   (0,1,0) → (0,3,0)             [1 hop, direct]
305        // BFS should pick the 1-hop direct route.
306        let mut registry = MigrationRegistry::new();
307        registry
308            .register(SetFieldMigration::new(
309                (0, 1, 0),
310                (0, 2, 0),
311                "intermediate",
312                json!(true),
313            ))
314            .register(SetFieldMigration::new(
315                (0, 2, 0),
316                (0, 3, 0),
317                "via_intermediate",
318                json!(true),
319            ))
320            .register(SetFieldMigration::new(
321                (0, 1, 0),
322                (0, 3, 0),
323                "direct",
324                json!(true),
325            ));
326
327        let plan = registry
328            .plan((0, 1, 0), (0, 3, 0))
329            .expect("plan should exist");
330        assert_eq!(plan.len(), 1, "BFS should pick the direct 1-hop path");
331
332        let mut ctx = MigrationContext::new(json!({}));
333        plan.apply(&mut ctx).expect("apply should succeed");
334        let doc = ctx.into_doc();
335        assert_eq!(doc.get("direct"), Some(&json!(true)));
336        assert_eq!(
337            doc.get("intermediate"),
338            None,
339            "2-hop path must not be taken"
340        );
341    }
342
343    #[test]
344    fn test_cycle_terminates() {
345        // Cycle: (0,1,0) → (0,2,0) → (0,1,0)
346        // Target (0,3,0) is unreachable; BFS must terminate and return error.
347        let mut registry = MigrationRegistry::new();
348        registry
349            .register(SetFieldMigration::new(
350                (0, 1, 0),
351                (0, 2, 0),
352                "fwd",
353                json!(1),
354            ))
355            .register(SetFieldMigration::new(
356                (0, 2, 0),
357                (0, 1, 0),
358                "back",
359                json!(2),
360            ));
361
362        let result = registry.plan((0, 1, 0), (0, 3, 0));
363        assert!(
364            matches!(result, Err(ServerError::Migration(_))),
365            "expected Migration error for unreachable target, got: {result:?}"
366        );
367    }
368
369    #[test]
370    fn test_step_descriptions() {
371        let mut registry = MigrationRegistry::new();
372        registry
373            .register(SetFieldMigration::new(
374                (0, 1, 0),
375                (0, 2, 0),
376                "f",
377                json!(null),
378            ))
379            .register(SetFieldMigration::new(
380                (0, 2, 0),
381                (0, 3, 0),
382                "g",
383                json!(null),
384            ));
385
386        let plan = registry.plan((0, 1, 0), (0, 3, 0)).expect("plan");
387        let descs = plan.step_descriptions();
388        assert_eq!(descs.len(), 2);
389        // Each description must be non-empty
390        for d in descs {
391            assert!(!d.is_empty());
392        }
393    }
394
395    #[test]
396    fn test_migration_context_set_get_remove() {
397        let mut ctx = MigrationContext::new(json!({"x": 1}));
398        assert_eq!(ctx.get("x"), Some(&json!(1)));
399        ctx.set("y", json!(2));
400        assert_eq!(ctx.get("y"), Some(&json!(2)));
401        ctx.remove("x");
402        assert_eq!(ctx.get("x"), None);
403        let doc = ctx.into_doc();
404        assert_eq!(doc.get("y"), Some(&json!(2)));
405    }
406
407    #[test]
408    fn test_registry_default() {
409        let registry = MigrationRegistry::default();
410        assert!(registry.migrations.is_empty());
411    }
412}