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 chrono::Utc;
use couchdb_orm::utils::{append_to_file, get_dir_entries, init_meta_dir};
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::BufRead;
use std::path::PathBuf;
use std::str::FromStr;
use thiserror::Error;

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

fn get_previous_version(db_path: &PathBuf) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
    if db_path.exists() {
        let entries: Vec<PathBuf> = get_dir_entries(&db_path)?;

        let entries: Vec<&PathBuf> = entries
            .iter()
            .filter(|res| !res.to_str().unwrap().contains(&"mod.rs"))
            .collect();

        if entries.len() > 0 {
            let mut entries: Vec<i64> = entries
                .iter()
                .map(|p| {
                    p.file_name()
                        .expect(&format!("could not get filename from {:?}", p))
                        .to_os_string()
                        .into_string()
                        .expect(&format!("could not convert {:?} to string", p))
                        .replace("schema_", "")
                        .replace(".rs", "")
                        .parse::<i64>()
                        .expect(&format!("could not convert {:?} to i64", p))
                })
                .collect();
            entries.sort();
            let last_version_name: PathBuf = entries
                .pop()
                .map(|o| {
                    PathBuf::from_str(&format!("schema_{}.rs", o))
                        .expect("could not convert to path")
                })
                .unwrap();
            let mut n_db_path: PathBuf = db_path.clone();
            n_db_path.push(last_version_name);
            Ok(Some(n_db_path))
        } else {
            Ok(None)
        }
    } else {
        Ok(None)
    }
}

fn write_file(db_path: &str, content: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
    let timestamp: i64 = Utc::now().timestamp();
    let file_name: String = format!("{}/schema_{}.rs", db_path, &timestamp);
    let mod_file_name: PathBuf = PathBuf::from_str(&format!("{}/mod.rs", db_path)).unwrap();
    let new_line: String = format!("pub mod schema_{};", &timestamp);

    fs::write(&file_name, &content)?;
    append_to_file(&mod_file_name, &new_line)?;
    Ok(PathBuf::from(file_name))
}

pub fn register_schema(
    path: PathBuf,
    db_name: String,
    override_version: bool,
    rootdir: &PathBuf,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
    init_meta_dir(&rootdir)?;
    let file_content: String = fs::read_to_string(&path).map_err(|e| {
        RegisterSchemaError(format!(
            "register_schema > read schema content error: {} -> {}",
            e,
            path.display()
        ))
    })?;
    let db_path: String = format!(
        "{}/_meta/src/schemas/{}",
        rootdir.to_str().unwrap(),
        db_name
    );

    let previous_version: Option<PathBuf> = get_previous_version(&PathBuf::from_str(&db_path)?)?;

    register_db(&db_name, rootdir)?;

    match previous_version {
        Some(ref p) => {
            if file_content == fs::read_to_string(p)? && !override_version {
                Err(Box::new(RegisterSchemaError(
                    "schema is same as last version.\naborting.".to_string(),
                )))
            } else if override_version {
                // remove last version
                fs::remove_file(p).map_err(|e| {
                    RegisterSchemaError(format!(
                        "register_schema > override_version > remove_file error: {}",
                        e
                    ))
                })?;
                // remove last line of mod file
                let mod_file_name: PathBuf =
                    PathBuf::from_str(&format!("{}/mod.rs", db_path)).unwrap();
                // open file
                let file = std::fs::File::open(&mod_file_name)?;
                let reader = std::io::BufReader::new(file);

                let mut mod_file_content: Vec<String> =
                    reader.lines().map(|l| l.unwrap()).collect();
                // delete lastline
                mod_file_content.pop().unwrap();
                let mod_file_content: String = mod_file_content.join("\n");
                fs::write(&mod_file_name, &mod_file_content).map_err(|e| {
                    RegisterSchemaError(format!(
                        "register_schema > override_version > write_file error: {} -> {}",
                        e,
                        mod_file_name.display()
                    ))
                })?;
                write_file(&db_path, &file_content)
            } else {
                write_file(&db_path, &file_content)
            }
        }
        None => write_file(&db_path, &file_content),
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use couchdb_orm::tests::utils::*;

    #[test]
    #[should_panic]
    fn test_get_previous_version_error() {
        create_tests_folder();

        let root_path = create_test_dir("get_previous_version_error");

        let files_to_create: Vec<PathBuf> = vec![
            root_path.join(PathBuf::from("not_a_version")),
            root_path.join(PathBuf::from("not_a_version_again")),
            root_path.join(PathBuf::from("not_a_version_again_again")),
        ];
        files_to_create.iter().for_each(|f| {
            fs::write(&f, "test").unwrap();
        });

        get_previous_version(&root_path).unwrap();
    }

    #[test]
    fn test_get_previous_version_no_dir() {
        let not_a_dir_path: PathBuf = PathBuf::from("not_a_dir");
        assert_eq!(get_previous_version(&not_a_dir_path).unwrap(), None);
    }

    #[test]
    fn test_get_previous_version_no_entries() {
        create_tests_folder();
        let root_path = create_test_dir("get_previous_version_no_entries");
        assert_eq!(get_previous_version(&root_path).unwrap(), None);
    }

    #[test]
    fn test_get_previous_version() {
        create_tests_folder();
        let root_path = create_test_dir("get_previous_version");

        let files_to_create: Vec<PathBuf> = vec![
            root_path.join(PathBuf::from("schema_0.rs")),
            root_path.join(PathBuf::from("schema_1.rs")),
            root_path.join(PathBuf::from("schema_2.rs")),
        ];
        files_to_create.iter().for_each(|f| {
            fs::write(&f, "test").unwrap();
        });
        assert_eq!(
            get_previous_version(&root_path).unwrap(),
            Some(root_path.join("schema_2.rs"))
        );
    }

    #[test]
    fn test_schema_write_file() {
        create_tests_folder();
        let root_path = create_test_dir("schema_write_file");

        let content: &str = "this is my content";
        let filepath: PathBuf = write_file(&root_path.to_str().unwrap(), &content).unwrap();
        let modpath: PathBuf = root_path.join("mod.rs");
        let file_content: String = fs::read_to_string(&filepath).unwrap();
        let mod_content: String = fs::read_to_string(&modpath).unwrap();
        let first_mod_content: String = format!(
            "pub mod {};",
            filepath
                .file_name()
                .unwrap()
                .to_str()
                .unwrap()
                .to_string()
                .replace(".rs", "")
        );

        assert!(filepath.exists());
        assert_eq!(&file_content, &content);
        assert_eq!(&mod_content, &first_mod_content);

        // 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();

        let filepath: PathBuf = write_file(&root_path.to_str().unwrap(), &content).unwrap();
        let file_content: String = fs::read_to_string(&filepath).unwrap();
        let mod_content: String = fs::read_to_string(&modpath).unwrap();

        assert!(filepath.exists());
        assert_eq!(&file_content, &content);
        assert_eq!(
            &mod_content,
            &format!(
                "{}\npub mod {};",
                first_mod_content,
                filepath
                    .file_name()
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .to_string()
                    .replace(".rs", "")
            )
        );
    }

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

        let root_path = create_test_dir("schema_same_version");

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

        let db_name: &str = "tasks";
        register_schema(filepath.clone(), db_name.to_string(), false, &root_path).unwrap();
        let error =
            register_schema(filepath.clone(), db_name.to_string(), false, &root_path).unwrap_err();
        let error: &RegisterSchemaError = error.downcast_ref().unwrap();
        assert_eq!(
            format!("{}", error),
            format!(
                "{}",
                RegisterSchemaError("schema is same as last version.\naborting.".to_string())
            )
        );
    }

    #[test]
    fn test_register_schema_override_last_version() {
        create_tests_folder();
        let root_path = create_test_dir("schema_override_last_version");

        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");
        let mod_path: PathBuf = root_path.join("_meta/src/schemas/tasks/mod.rs");
        fs::write(&filepath, &content).unwrap();
        fs::write(&filepath2, &content2).unwrap();

        let db_name: &str = "tasks";
        let schema_path_1: PathBuf =
            register_schema(filepath.clone(), db_name.to_string(), false, &root_path).unwrap();
        let schemas_content: String = fs::read_to_string(&schema_path_1).unwrap();
        let mod_content: String = fs::read_to_string(&mod_path).unwrap();

        assert!(schema_path_1.exists());
        assert_eq!(&schemas_content, &content);
        assert_eq!(
            &mod_content,
            &format!(
                "pub mod {};",
                schema_path_1
                    .file_name()
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .to_string()
                    .replace(".rs", "")
            )
        );

        // 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();

        let schema_path_2: PathBuf =
            register_schema(filepath2.clone(), db_name.to_string(), true, &root_path).unwrap();
        let mod_content: String = fs::read_to_string(&mod_path).unwrap();
        assert_ne!(&schema_path_1, &schema_path_2);
        assert_eq!(schema_path_1.exists(), false);
        assert_eq!(
            &mod_content,
            &format!(
                "pub mod {};",
                schema_path_2
                    .file_name()
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .to_string()
                    .replace(".rs", "")
            )
        );
    }

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

        let root_path = create_test_dir("register_schema");

        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";
        let schemas_path =
            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();

        let schemas_path2 =
            register_schema(filepath2.clone(), db_name.to_string(), false, &root_path).unwrap();
        let schemas_content: String = fs::read_to_string(&schemas_path).unwrap();
        let schemas_content2: String = fs::read_to_string(&schemas_path2).unwrap();

        assert!(schemas_path.exists());
        assert!(schemas_path2.exists());
        assert_eq!(&schemas_content, &content);
        assert_eq!(&schemas_content2, &content2);
    }
}