dsync 0.0.1

Generate rust structs & query functions from diesel schema files.
Documentation
mod code;
mod file;
mod parser;

use std::collections::HashMap;
use std::path::PathBuf;
use file::MarkedFile;
use parser::ParsedTableMacro;
pub use parser::FILE_SIGNATURE;

#[derive(Debug, Clone)]
pub struct TableOptions<'a> {
    pub ignore: bool,
    /// Names used for autogenerated columns which are NOT primary keys (for example: `created_at`, `updated_at`, etc.).
    pub autogenerated_columns: Vec<&'a str>,
    /// Adds #[tsync] attribute to structs (see https://github.com/Wulf/tsync)
    pub tsync: bool,
}


#[derive(Debug, Clone)]
pub struct GenerationConfig<'a> {
    pub table_options: HashMap<&'a str, TableOptions<'a>>,
    pub default_table_options: TableOptions<'a>
}

impl<'a> GenerationConfig<'_> {
    pub fn table(&self, name: &str) -> &TableOptions<'_> {
        &self.table_options.get(name).unwrap_or(&self.default_table_options)
    }
}

pub fn generate_code(diesel_schema_file_contents: String, config: GenerationConfig) -> anyhow::Result<Vec<ParsedTableMacro>> {
    parser::parse_and_generate_code(diesel_schema_file_contents, &config)
}

pub fn generate_files(input_diesel_schema_file: PathBuf, output_models_dir: PathBuf, config: GenerationConfig) {
    let input = input_diesel_schema_file;
    let output_dir = output_models_dir;

    let generated = generate_code(std::fs::read_to_string(input).expect("Could not read schema file."), config).expect("An error occurred.");

    if !output_dir.exists() {
        std::fs::create_dir(&output_dir).expect(&format!("Could not create directory '{:#?}'", output_dir));
    } else if !output_dir.is_dir() {
        panic!("Expected output argument to be a directory or non-existent.")
    }

    // check that the mod.rs file exists
    let mut mod_rs = MarkedFile::new(output_dir.clone().join("mod.rs"));

    // pass 1: add code for new tables
    for table in generated.iter() {
        let table_dir = output_dir.join(table.name.to_string());

        if !table_dir.exists() {
            std::fs::create_dir(&table_dir).expect(&format!("Could not create directory '{:#?}'", table_dir));
        }

        if !table_dir.is_dir() {
            panic!("Expected a directory at '{:#?}'", table_dir)
        }

        let mut table_generated_rs = MarkedFile::new(table_dir.join("generated.rs"));
        let mut table_mod_rs = MarkedFile::new(table_dir.join("mod.rs"));

        table_generated_rs.ensure_file_signature();
        table_generated_rs.file_contents = table.generated_code.clone();
        table_generated_rs.write();

        table_mod_rs.ensure_mod_stmt("generated");
        table_mod_rs.ensure_use_stmt("generated::*");
        table_mod_rs.write();

        mod_rs.ensure_mod_stmt(table.name.to_string().as_str());
    }

    // pass 2: delete code for removed tables
    for item in std::fs::read_dir(&output_dir).expect(&format!("Could not read directory '{:#?}'", output_dir)).into_iter() {
        let item = item.expect(&format!("Could not read item in '{:#?}'", output_dir));

        // check if item is a directory
        let file_type = item.file_type().expect(&format!("Could not determine type of file '{:#?}'", item.path()));
        if !file_type.is_dir() { continue }

        // check if it's a generated file
        let generated_rs_path = item.path().join("generated.rs");
        if !generated_rs_path.exists() || !generated_rs_path.is_file() || !MarkedFile::new(generated_rs_path.clone()).has_file_signature() { continue }

        // okay, it's generated, but we need to check if it is a deleted table
        let file_name = item.file_name();
        let associated_table_name = file_name.to_str().expect(&format!("Could not determine name of file '{:#?}'", item.path()));
        let found = generated.iter().find(|g| g.name.to_string().eq_ignore_ascii_case(associated_table_name));
        if found.is_some() { continue }

        // this table was deleted, let's delete the generated code
        std::fs::remove_file(&generated_rs_path).expect(&format!("Could not delete redundant file '{:#?}'", generated_rs_path));

        // remove the mod.rs file if there isn't anything left in there except the use stmt
        let table_mod_rs_path = item.path().join("mod.rs");
        if table_mod_rs_path.exists() {
            let mut table_mod_rs = MarkedFile::new(table_mod_rs_path);
            if table_mod_rs.file_contents.trim().eq(&format!("pub use generated::*;")) {
                table_mod_rs.delete();
            } else {
                table_mod_rs.remove_mod_stmt("generated");
                table_mod_rs.remove_use_stmt("generated::*");
                table_mod_rs.write();
            }
        }

        // delete the table dir if there's nothing else in there
        let is_empty = item.path().read_dir().expect(&format!("Could not read directory {:#?}", item.path())).next().is_none();
        if is_empty {
            std::fs::remove_dir(item.path()).expect(&format!("Could not delete directory '{:#?}'", item.path()));
        }

        // remove the module from the main mod_rs file
        mod_rs.remove_mod_stmt(associated_table_name);
    }

    mod_rs.write();
}