couchdb-orm 0.1.6

couchdb-orm Copyright (C) 2020-2023 OpenToolAdd This program comes with ABSOLUTELY NO WARRANTY; This is free software, and you are welcome to redistribute it under certain conditions; type `show-license' for details. A CLI ORM to manage some Databases operations like migration, schema creation, etc.... For the moment it only handle CouchDB.
Documentation
// Copyright (C) 2020-2023  OpenToolAdd
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
// contact: contact@tool-add.com

use crate::cli::args::actions::register::db::register_db;
use couchdb_orm::utils::{get_dir_entries, init_meta_dir, main_content};
use dialoguer::{theme::ColorfulTheme, MultiSelect};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use thiserror::Error;

#[derive(Debug, Error, Serialize, Deserialize, Clone)]
#[error("RegisterMigrationError: {0}")]
struct RegisterMigrationError(String);

fn write_main_file(rootdir: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
    let main_file_name: PathBuf =
        PathBuf::from_str(&format!("{}/_meta/src/main.rs", rootdir.to_str().unwrap())).unwrap();

    let main_content: String = main_content(rootdir)?;

    // println!("{:#?}", main_content);
    fs::write(main_file_name, main_content)?;

    Ok(())
}

fn write_migration_db_mod(
    db_path: &str,
    filename: &PathBuf,
    rootdir: &PathBuf,
) -> Result<(), Box<dyn std::error::Error>> {
    // println!("{}", db_path);
    let migrations_path: PathBuf = PathBuf::from_str(&db_path)?;
    let mod_file_name: PathBuf = PathBuf::from_str(&format!("{}/mod.rs", db_path)).unwrap();

    let available_migrations: Vec<PathBuf> = fs::read_dir(&migrations_path)?
        .map(|res| res.map(|e| e.path()))
        .collect::<Result<Vec<PathBuf>, std::io::Error>>()?;
    let available_migrations: Vec<&PathBuf> = available_migrations
        .iter()
        .filter(|e| e.file_name().unwrap() != "mod.rs")
        .collect();

    let available_migrations_mod_filenames: String = available_migrations
        .iter()
        .map(|e| {
            let mut s = e
                .file_name()
                .unwrap()
                .to_str()
                .unwrap()
                .to_string()
                .replace(".rs", ";");
            s.push_str("\n");
            format!("pub mod {}", s)
        })
        .collect();
    let mut available_migrations_enum: Vec<String> = available_migrations
        .iter()
        .map(|e| {
            e.file_name()
                .unwrap()
                .to_str()
                .unwrap()
                .to_string()
                .replace(".rs", "")
        })
        .collect();
    let filename_str: String = format!(
        r#"{}"#,
        filename
            .file_name()
            .unwrap()
            .to_str()
            .unwrap()
            .to_string()
            .replace(".rs", "")
    );
    if !available_migrations_enum.contains(&filename_str) {
        available_migrations_enum.push(filename_str.clone())
    }
    let available_migrations_enum: String = available_migrations_enum.join(",\n");
    let mut available_migrations_list: Vec<String> = available_migrations
        .iter()
        .map(|e| {
            let s = e
                .file_name()
                .unwrap()
                .to_str()
                .unwrap()
                .to_string()
                .replace(".rs", "");
            let s = format!(r#""{}""#, s);
            s
        })
        .collect();
    let filename_str: String = format!(r#""{}""#, &filename_str);

    if !available_migrations_list.contains(&filename_str) {
        available_migrations_list.push(filename_str.clone());
    }
    let available_migrations_list: String = available_migrations_list.join(",\n");
    let available_migration_content: String = format!(
        include_str!("../../../../../../../static/templates/available_migrations.tpl"),
        available_migrations_mod_filenames, available_migrations_enum, available_migrations_list
    );
    // println!("{:?}", available_migration_content);
    fs::write(mod_file_name, available_migration_content)?;

    write_main_file(rootdir)?;
    Ok(())
}

fn write_migration_file(
    db_path: &str,
    db_name: &str,
    filenames: (String, String),
    override_version: bool,
    rootdir: &PathBuf,
) -> Result<(), Box<dyn std::error::Error>> {
    let src_name: String = filenames.0.replace(".rs", "");
    let dest_name: String = filenames.1.replace(".rs", "");
    let file_content: String = format!(
        include_str!("../../../../../../../static/templates/migration_file.tpl"),
        db_name, src_name, db_name, dest_name, db_name
    );
    let file_name: PathBuf = PathBuf::from_str(&format!(
        "{}/from_{}_to_{}.rs",
        db_path, src_name, dest_name
    ))
    .unwrap();

    if !file_name.exists() || override_version {
        fs::write(&file_name, &file_content)?;
    }

    write_migration_db_mod(db_path, &file_name, rootdir)?;
    Ok(())
}

pub fn register_migration(
    db_name: String,
    override_version: bool,
    rootdir: &PathBuf,
) -> Result<(), Box<dyn std::error::Error>> {
    init_meta_dir(&rootdir)?;
    let db_path: PathBuf = PathBuf::from_str(&format!(
        "{}/_meta/src/migrations/{}",
        rootdir.to_str().unwrap(),
        db_name
    ))?;
    let schemas_path: PathBuf = PathBuf::from_str(&format!(
        "{}/_meta/src/schemas/{}",
        rootdir.to_str().unwrap(),
        db_name
    ))?;

    register_db(&db_name, rootdir)?;

    let schemas_available: Vec<PathBuf> = get_dir_entries(&schemas_path)?;
    let schemas_available: Vec<&str> = schemas_available
        .iter()
        .filter(|res| !res.to_str().unwrap().contains(&"mod.rs"))
        .map(|s| s.to_str().unwrap())
        .collect();

    if schemas_available.len() > 0 {
        let selection: Vec<usize> = MultiSelect::with_theme(&ColorfulTheme::default())
            .with_prompt("Select 2 schemas to create a migration: ")
            .items(&schemas_available)
            .interact()?;

        if selection.len() != 2 {
            Err(Box::new(RegisterMigrationError(String::from(
                "2 schemas must be selected to register a migration",
            ))))
        } else {
            let selection: Vec<PathBuf> = selection
                .iter()
                .cloned()
                .map(|i| PathBuf::from_str(schemas_available[i]).unwrap())
                .collect();
            let filenames: Vec<String> = selection
                .iter()
                .map(|p| p.file_name().unwrap().to_os_string().into_string().unwrap())
                .collect();

            // println!("{:?}", filenames);
            write_migration_file(
                &db_path.to_str().unwrap(),
                &db_name,
                (filenames[0].clone(), filenames[1].clone()),
                override_version,
                rootdir,
            )
        }
    } else {
        Err(Box::new(RegisterMigrationError(format!(
            "No schemas registered in {}",
            schemas_path.to_str().unwrap()
        ))))
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::cli::args::actions::register::schema::register_schema;
    use couchdb_orm::tests::utils::*;

    #[test]
    fn test_register_migration() {
        create_tests_folder();

        let root_path = create_test_dir("register_migration");

        let content: &str = "this is my content";
        let content2: &str = "this is my content2";
        let filepath: PathBuf = root_path.join("test_model");
        let filepath2: PathBuf = root_path.join("test_model_2");
        fs::write(&filepath, &content).unwrap();
        fs::write(&filepath2, &content2).unwrap();

        let db_name: &str = "tasks";

        register_schema(filepath.clone(), db_name.to_string(), false, &root_path).unwrap();
        // wait for time before writing new file
        // or the timestamps are the same
        let mut child = std::process::Command::new("sleep")
            .arg("1")
            .spawn()
            .unwrap();
        let _result = child.wait().unwrap();
        register_schema(filepath2.clone(), db_name.to_string(), false, &root_path).unwrap();
        // for the moment it asks for user input
        // with the call to register migration
        // cannot be automated.
        // @TODO automatize
        // register_migration(
        //     db_name.to_string(),
        //     false,
        //     &root_path
        // ).unwrap();
        // let migrations_path: PathBuf = root_path.join("_meta/src/migrations/tasks");
        // // println!("{:?}", migrations_path);
        // let entries: Vec<PathBuf> = get_dir_entries(&migrations_path).unwrap();
        // let entries: Vec<&PathBuf> = entries.iter().filter(|f| !f.file_name().unwrap().to_str().unwrap().contains(&"mod.rs")).collect();
        // assert!(entries.len() == 1);
        assert!(true)
    }
}