dsync 0.0.1

Generate rust structs & query functions from diesel schema files.
Documentation
use indoc::indoc;
use inflector::Inflector;
use crate::GenerationConfig;
use crate::parser::{FILE_SIGNATURE, ParsedTableMacro};

#[derive(PartialEq, Eq)]
enum StructType {
    Create,
    Read,
    Update
}

impl StructType {
    pub fn prefix(&self) -> &'static str {
        match self {
            StructType::Create => "New",
            StructType::Read => "",
            StructType::Update => "Update"
        }
    }
}

fn build_table_struct(ty: StructType, table: &ParsedTableMacro, config: &GenerationConfig) -> String {
    let table_options = config.table(table.name.to_string().as_str());

    let primary_keys: Vec<String> = table.primary_key_columns.iter().map(|i| i.to_string()).collect();
    let belongs_to = table.foreign_keys.iter().map(|fk| format!("belongs_to({foreign_table_name}, foreign_key={join_column})", foreign_table_name=fk.0.to_string().to_pascal_case(), join_column=fk.1.to_string())).collect::<Vec<String>>().join(", ");

    let struct_code = format!(
        indoc! {r#"
            {tsync_attr}#[derive(Debug, Serialize, Deserialize, Clone, Queryable, Insertable, AsChangeset{derive_identifiable}{derive_associations})]
            #[diesel(table_name={table_name}{primary_key}{belongs_to})]
            pub struct {struct_prefix}{struct_name} {{
            $COLUMNS$
            }}
        "#},
        tsync_attr = if table_options.tsync { "#[tsync::tsync]\n" } else { "" },
        struct_prefix = ty.prefix(),
        table_name = table.name.to_string(),
        struct_name = table.struct_name,
        primary_key = if ty != StructType::Read {"".to_string()} else {format!(", primary_key({})", primary_keys.join(","))},
        derive_associations = if belongs_to.len() > 0 { ", Associations" } else { "" },
        derive_identifiable = if ty == StructType::Read { ", Identifiable" } else { "" },
        belongs_to = belongs_to
    );

    let mut struct_columns: Vec<String> = vec![];
    for col in table.columns.iter() {
        let field_name = col.name.to_string();
        let mut field_type = col.ty.to_string();

        // skip fields that are readonly in create/update structs
        if ty != StructType::Read && table_options.autogenerated_columns.clone().contains(&field_name.as_str()) {
            continue
        }

        // skip fields that are primary keys in create/update structs
        let is_pk = table.primary_key_columns.iter().find(|pk| pk.to_string().eq(field_name.as_str())).is_some();
        if is_pk && ty != StructType::Read {
            continue
        }

        // check if it's nullable (optional)
        if col.is_nullable {
            field_type = format!("Option<{}>", col.ty.to_string());
        }

        struct_columns.push(format!(r#"    {field_name}: {field_type},"#));
    }

    struct_code.replace("$COLUMNS$", &struct_columns.join("\n"))
}

fn build_table_structs(table: &ParsedTableMacro, config: &GenerationConfig) -> String {
    let mut structs = String::new();

    let read_struct = build_table_struct(StructType::Read, table, config);
    let update_struct = build_table_struct(StructType::Update, table, config);
    let create_struct = build_table_struct(StructType::Create, table, config);

    structs.push_str(read_struct.as_str());
    structs.push('\n');
    structs.push_str(update_struct.as_str());
    structs.push('\n');
    structs.push_str(create_struct.as_str());

    structs
}


fn build_table_fns(table: &ParsedTableMacro, config: &GenerationConfig) -> String {
    let primary_column_name_and_type: Vec<(String, String)> = table.primary_key_columns.iter().map(|pk| {
        let col = table.columns.iter().find(|it| it.name.to_string().eq(pk.to_string().as_str())).expect("Primary key column doesn't exist in table");

        (col.name.to_string(), col.ty.to_string())
    }).collect();

    let item_id_params = primary_column_name_and_type.iter().map(|name_and_type| format!("param_{name}: {ty}", name=name_and_type.0, ty=name_and_type.1)).collect::<Vec<String>>().join(", ");
    let item_id_filters = primary_column_name_and_type.iter().map(|name_and_type| format!("filter({name}.eq(param_{name}))", name=name_and_type.0.to_string())).collect::<Vec<String>>().join(".");

    format!(
        indoc!{"
            pub fn create(db: &mut Connection, item: &New{struct_name}) -> QueryResult<{struct_name}> {{
                use crate::schema::{table_name}::dsl::*;

                insert_into({table_name}).values(item).get_result::<{struct_name}>(db)
            }}

            pub fn read(db: &mut Connection, {item_id_params}) -> QueryResult<{struct_name}> {{
                use crate::schema::{table_name}::dsl::*;

                {table_name}.{item_id_filters}.first::<{struct_name}>(db)
            }}

            pub fn paginate(db: &mut Connection, page: i64, page_size: i64) -> QueryResult<Vec<{struct_name}>> {{
                use crate::schema::{table_name}::dsl::*;

                {table_name}.limit(page_size).offset(page * page_size).load::<{struct_name}>(db)
            }}

            pub fn update(db: &mut Connection, {item_id_params}, item: &Update{struct_name}) -> QueryResult<{struct_name}> {{
                use crate::schema::{table_name}::dsl::*;

                diesel::update({table_name}.{item_id_filters}).set(item).get_result(db)
            }}

            pub fn delete(db: &mut Connection, {item_id_params}) -> QueryResult<usize> {{
                use crate::schema::{table_name}::dsl::*;

                diesel::delete({table_name}.{item_id_filters}).execute(db)
            }}
        "},
        table_name = table.name.to_string(),
        struct_name = table.struct_name,
        item_id_params = item_id_params,
        item_id_filters = item_id_filters
    )
}

fn build_imports(config: &GenerationConfig) -> String {
    (indoc!{"
        use crate::diesel::*;
        use crate::schema::*;

        use create_rust_app::Connection;
        use diesel::QueryResult;
        use serde::{Deserialize, Serialize};
    "}).to_string()
}

pub fn generate_table(table: ParsedTableMacro, config: &GenerationConfig) -> String {
    let table_structs = build_table_structs(&table, config);
    let table_fns = build_table_fns(&table, config);
    let imports = build_imports(config);

    format!("{FILE_SIGNATURE}\n\n{imports}\n{table_structs}\n{table_fns}")
}