sofie-cli 0.4.0

The `sofie` command: gen, check and the tools reading the app model.
Documentation
//! Applied like any migration, a role migration is never rewritten, so a later template leaves it as it is.

use std::path::{Path, PathBuf};

use sofie_model::{App, Module};

const TEMPLATE: &str = include_str!("../module_role.sql");

const FILE_NAME: &str = "0000_module_role.sql";

const SQLITE_TEXT: &str =
    "-- @generated by `sofie gen` once and never rewritten: SQLite has no database roles.\n";

pub(crate) fn missing_role_migrations(app: &App) -> Vec<(PathBuf, String)> {
    app.modules
        .iter()
        .filter(|module| !module.migrations.is_empty())
        .flat_map(role_migrations)
        .filter(|(path, _)| !path.exists())
        .collect()
}

fn role_migrations(module: &Module) -> Vec<(PathBuf, String)> {
    let folder = module.krate.path.join("migrations");
    let postgres = TEMPLATE.replace("{module}", &module.krate.crate_name);
    let mut files = vec![(folder.join(FILE_NAME), postgres)];
    if !module.sqlite_migrations.is_empty() {
        files.push((
            folder.join("sqlite").join(FILE_NAME),
            SQLITE_TEXT.to_owned(),
        ));
    }
    files
}

pub(crate) fn with_role_migrations(app: &App, missing: &[(PathBuf, String)]) -> App {
    let mut app = app.clone();
    for module in &mut app.modules {
        let folder = module.krate.path.join("migrations");
        add_in_folder(&mut module.migrations, missing, &folder);
        add_in_folder(
            &mut module.sqlite_migrations,
            missing,
            &folder.join("sqlite"),
        );
    }
    app
}

fn add_in_folder(migrations: &mut Vec<PathBuf>, missing: &[(PathBuf, String)], folder: &Path) {
    migrations.extend(
        missing
            .iter()
            .map(|(path, _)| path)
            .filter(|path| path.parent() == Some(folder))
            .cloned(),
    );
    migrations.sort();
}

#[cfg(test)]
mod tests {
    use sofie_model::testing::{TemporaryDirectory, app, module};

    use super::*;
    use crate::gen_command::{GenMode, GenOutcome, sync_file};

    #[test]
    fn a_module_with_migrations_gets_its_role_migration_first_and_once() {
        let directory = TemporaryDirectory::new("sofie-module-roles");
        let mut catalog = module("catalog", directory.path("modules/catalog"));
        catalog.migrations = vec![directory.write(
            "modules/catalog/migrations/0001_products.sql",
            "CREATE TABLE product (id uuid PRIMARY KEY);",
        )];
        let empty = module("search", directory.path("modules/search"));
        let model = app(directory.root(), vec![catalog, empty]);

        let missing = missing_role_migrations(&model);
        let with_roles = with_role_migrations(&model, &missing);

        let role_file = directory.path("modules/catalog/migrations/0000_module_role.sql");
        assert_eq!(missing.len(), 1);
        assert_eq!(missing[0].0, role_file);
        assert!(
            missing[0]
                .1
                .contains("SELECT sofie.module_role('catalog');")
        );
        assert!(!missing[0].1.contains("{module}"));
        assert_eq!(with_roles.modules[0].migrations[0], role_file);
        let text = Some(missing[0].1.clone());
        assert_eq!(
            sync_file(&role_file, text.clone(), GenMode::Check).unwrap(),
            Some(GenOutcome::Stale(role_file.clone()))
        );
        sync_file(&role_file, text, GenMode::Write).unwrap();
        assert_eq!(missing_role_migrations(&model), []);
    }

    #[test]
    fn a_module_with_sqlite_migrations_gets_a_sqlite_role_migration_too() {
        let directory = TemporaryDirectory::new("sofie-module-roles-sqlite");
        let mut catalog = module("catalog", directory.path("modules/catalog"));
        catalog.migrations = vec![directory.write(
            "modules/catalog/migrations/0001_products.sql",
            "CREATE TABLE product (id uuid PRIMARY KEY);",
        )];
        catalog.sqlite_migrations = vec![directory.write(
            "modules/catalog/migrations/sqlite/0001_products.sql",
            "CREATE TABLE catalog_product (id TEXT PRIMARY KEY);",
        )];
        let model = app(directory.root(), vec![catalog]);

        let missing = missing_role_migrations(&model);
        let with_roles = with_role_migrations(&model, &missing);

        let sqlite_file = directory.path("modules/catalog/migrations/sqlite/0000_module_role.sql");
        assert_eq!(missing.len(), 2);
        assert_eq!(missing[1].0, sqlite_file);
        assert!(!missing[1].1.contains("SELECT"));
        assert_eq!(with_roles.modules[0].sqlite_migrations[0], sqlite_file);
        assert_eq!(
            with_roles.modules[0].migrations[0],
            directory.path("modules/catalog/migrations/0000_module_role.sql")
        );
    }
}