cdk-sql-common 0.13.0

Generic SQL storage backend for CDK
Documentation
use std::cmp::Ordering;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};

fn main() {
    // Step 1: Find `migrations/` folder recursively
    let root = Path::new("src");

    // Get the OUT_DIR from Cargo - this is writable
    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set by Cargo"));

    for migration_path in find_migrations_dirs(root) {
        // Step 3: Output file path to OUT_DIR instead of source directory
        let parent = migration_path.parent().unwrap();

        // Create a unique filename based on the migration path to avoid conflicts
        let migration_name = parent
            .strip_prefix("src")
            .unwrap_or(parent)
            .to_str()
            .unwrap_or("default")
            .replace("/", "_")
            .replace("\\", "_");
        let dest_path = out_dir.join(format!("migrations_{}.rs", migration_name));
        let mut out_file = File::create(&dest_path).expect("Failed to create migrations.rs");

        let skip_name = migration_path.to_str().unwrap_or_default().len();

        // Step 2: Collect all files inside the migrations dir
        let mut files = Vec::new();
        visit_dirs(&migration_path, &mut files).expect("Failed to read migrations directory");
        files.sort_by(|path_a, path_b| {
            let parts_a = path_a.to_str().unwrap().replace("\\", "/")[skip_name + 1..]
                .split("/")
                .map(|x| x.to_owned())
                .collect::<Vec<_>>();
            let parts_b = path_b.to_str().unwrap().replace("\\", "/")[skip_name + 1..]
                .split("/")
                .map(|x| x.to_owned())
                .collect::<Vec<_>>();

            let prefix_a = if parts_a.len() == 2 {
                parts_a.first().map(|x| x.to_owned()).unwrap_or_default()
            } else {
                "".to_owned()
            };

            let prefix_b = if parts_a.len() == 2 {
                parts_b.first().map(|x| x.to_owned()).unwrap_or_default()
            } else {
                "".to_owned()
            };

            let prefix_cmp = prefix_a.cmp(&prefix_b);

            if prefix_cmp != Ordering::Equal {
                return prefix_cmp;
            }

            let path_a = path_a.file_name().unwrap().to_str().unwrap();
            let path_b = path_b.file_name().unwrap().to_str().unwrap();

            let prefix_a = path_a
                .split("_")
                .next()
                .and_then(|prefix| prefix.parse::<usize>().ok())
                .unwrap_or_default();
            let prefix_b = path_b
                .split("_")
                .next()
                .and_then(|prefix| prefix.parse::<usize>().ok())
                .unwrap_or_default();

            if prefix_a != 0 && prefix_b != 0 {
                prefix_a.cmp(&prefix_b)
            } else {
                path_a.cmp(path_b)
            }
        });

        writeln!(out_file, "/// @generated").unwrap();
        writeln!(out_file, "/// Auto-generated by build.rs").unwrap();
        writeln!(
            out_file,
            "pub static MIGRATIONS: &[(&str, &str, &str)] = &["
        )
        .unwrap();

        for path in &files {
            let parts = path.to_str().unwrap().replace("\\", "/")[skip_name + 1..]
                .split("/")
                .map(|x| x.to_owned())
                .collect::<Vec<_>>();

            let prefix = if parts.len() == 2 {
                parts.first().map(|x| x.to_owned()).unwrap_or_default()
            } else {
                "".to_owned()
            };

            let rel_name = &path.file_name().unwrap().to_str().unwrap();

            // Copy migration file to OUT_DIR
            let relative_path = path.strip_prefix(root).unwrap();
            let dest_migration_file = out_dir.join(relative_path);
            if let Some(parent) = dest_migration_file.parent() {
                fs::create_dir_all(parent)
                    .expect("Failed to create migration directory in OUT_DIR");
            }
            fs::copy(path, &dest_migration_file).expect("Failed to copy migration file to OUT_DIR");

            // Use path relative to OUT_DIR for include_str
            let relative_to_out_dir = relative_path.to_str().unwrap().replace("\\", "/");
            writeln!(
                out_file,
                "    (\"{prefix}\", \"{rel_name}\", include_str!(r#\"{}\"#)),",
                relative_to_out_dir
            )
            .unwrap();
            println!("cargo:rerun-if-changed={}", path.display());
        }

        writeln!(out_file, "];").unwrap();

        println!("cargo:rerun-if-changed={}", migration_path.display());
    }
}

fn find_migrations_dirs(root: &Path) -> Vec<PathBuf> {
    let mut found = Vec::new();
    find_migrations_dirs_rec(root, &mut found);
    found
}

fn find_migrations_dirs_rec(dir: &Path, found: &mut Vec<PathBuf>) {
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                if path.file_name().unwrap_or_default() == "migrations" {
                    found.push(path.clone());
                }
                find_migrations_dirs_rec(&path, found);
            }
        }
    }
}

fn visit_dirs(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            visit_dirs(&path, files)?;
        } else if path.is_file() {
            files.push(path);
        }
    }
    Ok(())
}