Skip to main content

rust_bucket/
migrations.rs

1// Version migration support
2//
3// Embeds markdown migration files from the migrations/ directory and provides
4// a function to retrieve migrations between two versions.
5
6use rust_embed::RustEmbed;
7use semver::Version;
8use thiserror::Error;
9
10/// Embedded migration files from the migrations/ directory
11#[derive(RustEmbed)]
12#[folder = "migrations/"]
13struct MigrationFiles;
14
15/// A single version migration with instructions
16#[derive(Debug, Clone)]
17pub struct Migration {
18    pub version: Version,
19    pub instructions: String,
20}
21
22/// Errors that can occur when working with migrations
23#[derive(Debug, Error)]
24pub enum MigrationError {
25    #[error("Failed to parse version '{0}': {1}")]
26    VersionParse(String, semver::Error),
27}
28
29/// Returns all migrations between two versions (exclusive of `from`, inclusive of `to`).
30///
31/// Migrations are returned sorted by version in ascending order.
32/// Filenames that don't parse as semver versions are silently skipped.
33///
34/// Returns an empty Vec if `from >= to`.
35pub fn migrations_between(from: &Version, to: &Version) -> Result<Vec<Migration>, MigrationError> {
36    if from >= to {
37        return Ok(Vec::new());
38    }
39
40    let mut migrations = Vec::new();
41
42    for filename in MigrationFiles::iter() {
43        // Strip .md extension to get version string
44        let version_str = match filename.strip_suffix(".md") {
45            Some(v) => v,
46            None => continue,
47        };
48
49        // Parse version, skip files that don't parse
50        let version = match Version::parse(version_str) {
51            Ok(v) => v,
52            Err(_) => continue,
53        };
54
55        // Check if this migration is in range (from < version <= to)
56        if version > *from
57            && version <= *to
58            && let Some(file) = MigrationFiles::get(&filename)
59        {
60            let instructions = String::from_utf8_lossy(&file.data).to_string();
61            migrations.push(Migration {
62                version,
63                instructions,
64            });
65        }
66    }
67
68    // Sort by version ascending
69    migrations.sort_by(|a, b| a.version.cmp(&b.version));
70
71    Ok(migrations)
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_migrations_between_includes_060() {
80        let from = Version::new(0, 5, 0);
81        let to = Version::new(0, 6, 0);
82        let result = migrations_between(&from, &to).unwrap();
83        assert_eq!(result.len(), 1);
84        assert_eq!(result[0].version, Version::new(0, 6, 0));
85    }
86
87    #[test]
88    fn test_migrations_between_same_version_returns_empty() {
89        let v = Version::new(0, 6, 0);
90        let result = migrations_between(&v, &v).unwrap();
91        assert!(result.is_empty());
92    }
93
94    #[test]
95    fn test_migrations_between_reversed_returns_empty() {
96        let from = Version::new(0, 7, 0);
97        let to = Version::new(0, 5, 0);
98        let result = migrations_between(&from, &to).unwrap();
99        assert!(result.is_empty());
100    }
101
102    #[test]
103    fn test_migration_content_is_non_empty() {
104        let from = Version::new(0, 5, 0);
105        let to = Version::new(0, 6, 0);
106        let result = migrations_between(&from, &to).unwrap();
107        assert!(!result.is_empty());
108        for migration in &result {
109            assert!(!migration.instructions.is_empty());
110        }
111    }
112}