rorm-cli 0.10.1

The cli tool for managing rorm applications.
Documentation
use std::fs::{read_dir, read_to_string, DirEntry, File};
use std::io::Write;
use std::path::Path;

use anyhow::{anyhow, Context};
use rorm_declaration::imr::InternalModelFormat;
use rorm_declaration::migration::{Migration, MigrationFile};

use crate::utils::imr_as_state::InternalModelFormatExt;
use crate::utils::re::RE;

/**
This function is used to convert the [InternalModelFormat] into its TOML representation.

`migration` [Migration]: Migration to be converted into TOML
`path` [&str]: The path to write the resulting TOML to
 */
pub fn convert_migration_to_file(migration: Migration, path: &Path) -> anyhow::Result<()> {
    let toml_str = toml::to_string_pretty(&MigrationFile { migration })
        .with_context(|| "Error while serializing migration")?;

    let mut output = File::create(path).with_context(|| {
        format!(
            "Error while opening file {:?} to write migration into",
            path.file_name()
        )
    })?;
    write!(output, "{toml_str}").with_context(|| "Error while writing to migration file")?;

    Ok(())
}

/**
This function tries to convert a file to a [Migration].

`path` [&DirEntry]: Path to the file that should be parsed.
*/
pub fn convert_file_to_migration(path: &DirEntry) -> anyhow::Result<MigrationFile> {
    let toml_str = read_to_string(path.path()).with_context(|| {
        format!(
            "Error occurred while reading {}",
            path.path().to_str().unwrap()
        )
    })?;

    let mut migration: MigrationFile = toml::from_str(toml_str.as_str()).with_context(|| {
        format!(
            "Error while deserializing migration {:?} from TOML",
            path.file_name()
        )
    })?;

    migration.migration.id = path.path().file_stem().unwrap().to_str().unwrap()[..4].parse()?;
    migration.migration.name = path.path().file_stem().unwrap().to_str().unwrap()[5..].to_string();

    Ok(migration)
}

pub(crate) fn get_migration_files(migration_dir: &str) -> anyhow::Result<Vec<DirEntry>> {
    let dir_entries =
        read_dir(migration_dir).with_context(|| "Error while searching the migration directory")?;

    let file_list: Vec<DirEntry> = dir_entries
        .filter(|x| {
            x.as_ref().unwrap().file_type().unwrap().is_file()
                && RE.migration_allowed_name.is_match(
                    x.as_ref()
                        .unwrap()
                        .file_name()
                        .into_string()
                        .unwrap()
                        .as_str(),
                )
        })
        .map(|x| x.unwrap())
        .collect();

    Ok(file_list)
}

/**
Helper function to retrieve a sorted list of migrations in a given directory.

This strips also migrations, that were replaced.

**Parameter**:
- `migration_dir`: [&str] The directory to search for files.
  this point onwards.
*/
pub fn get_existing_migrations(migration_dir: &str) -> anyhow::Result<Vec<Migration>> {
    let migrations = get_all_existing_migrations(migration_dir)?;

    let mut migration_list: Vec<Migration> = vec![];

    // Filter out migrations that replace migrations
    for m in migrations {
        if m.replaces.is_empty() {
            migration_list.push(m);
        }
    }

    let mut sorted_migration_list: Vec<Migration> = vec![];

    let mut current_id = None;
    loop {
        match current_id {
            None => {
                if let Some(&initial) = migration_list
                    .iter()
                    .filter(|x| x.initial)
                    .collect::<Vec<&Migration>>()
                    .first()
                {
                    current_id = Some(initial.id);
                    sorted_migration_list.push(initial.clone());
                    continue;
                }
            }
            Some(curr) => {
                if let Some(&next) = migration_list
                    .iter()
                    .filter(|x| {
                        if let Some(dependency) = x.dependency {
                            dependency == curr
                        } else {
                            false
                        }
                    })
                    .collect::<Vec<&Migration>>()
                    .first()
                {
                    current_id = Some(next.id);
                    sorted_migration_list.push(next.clone());
                    continue;
                }
            }
        }
        break;
    }

    if sorted_migration_list.len() != migration_list.len() {
        return Err(anyhow!("Migrations does not assemble to a coherent list."));
    }

    Ok(sorted_migration_list)
}

/**
Helper function to retrieve an unsorted list of **all** migrations in a given directory.

`migration_dir`: [&str] The directory to search for files.
 */
pub fn get_all_existing_migrations(migration_dir: &str) -> anyhow::Result<Vec<Migration>> {
    let file_list = get_migration_files(migration_dir)?;
    let mut migration_list: Vec<Migration> = vec![];
    for file in &file_list {
        migration_list.push(convert_file_to_migration(file)?.migration);
    }

    Ok(migration_list)
}

/**
Helper function to converts a list of migrations to an internal model.

`migrations`: [Vec<Migration>]: List of migrations
 */
pub fn convert_migrations_to_internal_models(
    migrations: &[Migration],
) -> anyhow::Result<InternalModelFormat> {
    let mut state = InternalModelFormat { models: Vec::new() };

    for migration in migrations {
        for operation in &migration.operations {
            state.apply_operation(operation)?;
        }
    }

    Ok(state)
}

#[cfg(test)]
mod test {
    use std::path::Path;

    use rorm_declaration::migration::Migration;
    use temp_dir::TempDir;

    use crate::utils::migrations::{convert_migration_to_file, get_existing_migrations};

    #[test]
    fn test_get_existing_migrations_non_initial() {
        let tmp = TempDir::new().expect("Could not create a temporary directory");
        let p = tmp.path().join("0001_not_initial.toml");

        let migration = Migration {
            hash: "".to_string(),
            initial: false,
            id: 0,
            name: "".to_string(),
            dependency: None,
            replaces: vec![],
            operations: vec![],
        };

        convert_migration_to_file(migration, Path::new(p.to_str().unwrap()))
            .expect("Could not write to file");

        assert!(get_existing_migrations(tmp.path().to_str().unwrap()).is_err());
    }

    #[test]
    fn test_get_existing_migrations_initial() {
        let tmp = TempDir::new().expect("Could not create a temporary directory");
        let p = tmp.path().join("0001_initial.toml");

        let migration = Migration {
            hash: "".to_string(),
            initial: true,
            id: 0,
            name: "".to_string(),
            dependency: None,
            replaces: vec![],
            operations: vec![],
        };

        convert_migration_to_file(migration, Path::new(p.to_str().unwrap()))
            .expect("Could not write to file");

        assert!(get_existing_migrations(tmp.path().to_str().unwrap()).is_ok());
    }

    #[test]
    fn test_get_existing_migrations_multiple_connected() {
        let tmp = TempDir::new().expect("Could not create a temporary directory");
        let p = tmp.path().join("0001_initial.toml");
        let p_2 = tmp.path().join("00002_foobar.toml");

        let migration = Migration {
            hash: "".to_string(),
            initial: true,
            id: 0,
            name: "".to_string(),
            dependency: None,
            replaces: vec![],
            operations: vec![],
        };

        convert_migration_to_file(migration, Path::new(p.to_str().unwrap()))
            .expect("Could not write to file");

        let migration = Migration {
            hash: "".to_string(),
            initial: false,
            id: 0,
            name: "".to_string(),
            dependency: Some(1),
            replaces: vec![],
            operations: vec![],
        };

        convert_migration_to_file(migration, Path::new(p_2.to_str().unwrap()))
            .expect("Could not write to file");

        assert!(get_existing_migrations(tmp.path().to_str().unwrap()).is_ok());
    }
}