dsync 0.0.6

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

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

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

    pub fn suffix(&self) -> &'static str {
        match self {
            StructType::Form => "Form",
            StructType::Read => "",
        }
    }

    /// returns a struct name for this struct type given a base name
    pub fn format(&self, name: &'_ str) -> String {
        format!(
            "{struct_prefix}{struct_name}{struct_suffix}",
            struct_prefix = self.prefix(),
            struct_name = name,
            struct_suffix = self.suffix()
        )
    }
}

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().to_singular(),
                join_column = fk.1
            )
        })
        .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_name} {{
            $COLUMNS$
            }}
        "#},
        tsync_attr = if table_options.get_tsync() {
            "#[tsync::tsync]\n"
        } else {
            ""
        },
        table_name = table.name,
        struct_name = ty.format(table.struct_name.as_str()),
        primary_key = if ty != StructType::Read {
            "".to_string()
        } else {
            format!(", primary_key({})", primary_keys.join(","))
        },
        derive_associations = if ty == StructType::Read && !belongs_to.is_empty() {
            ", Associations"
        } else {
            ""
        },
        derive_identifiable = if ty == StructType::Read {
            ", Identifiable"
        } else {
            ""
        },
        belongs_to = if ty != StructType::Read {
            "".to_string()
        } else {
            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
                .get_autogenerated_columns()
                .contains(&field_name.as_str())
        {
            continue;
        }

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

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

        struct_columns.push(format!(r#"    pub {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 form_struct = build_table_struct(StructType::Form, table, config);

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

    structs
}

fn build_table_fns(table: &ParsedTableMacro, config: &GenerationConfig) -> String {
    let table_config = config.table(&table.name.to_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! {r##"
            {tsync}
            #[derive(Serialize)]
            pub struct PaginationResult<T> {{
                pub items: Vec<T>,
                pub total_items: i64,
                /// 0-based index
                pub page: i64,
                pub page_size: i64,
                pub num_pages: i64,
            }}

            impl {struct_name} {{
                pub fn create(db: &mut Connection, item: &{form_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)
                }}

                /// Paginates through the table where page is a 0-based index (i.e. page 0 is the first page)
                pub fn paginate(db: &mut Connection, page: i64, page_size: i64) -> QueryResult<PaginationResult<{struct_name}>> {{
                    use crate::schema::{table_name}::dsl::*;

                    let page_size = if page_size < 1 {{ 1 }} else {{ page_size }};
                    let total_items = {table_name}.count().get_result(db)?;
                    let items = {table_name}.limit(page_size).offset(page * page_size).load::<{struct_name}>(db)?;

                    Ok(PaginationResult {{
                        items,
                        total_items,
                        page,
                        page_size,
                        /* ceiling division of integers */
                        num_pages: total_items / page_size + i64::from(total_items % page_size != 0)
                    }})
                }}

                pub fn update(db: &mut Connection, {item_id_params}, item: &{form_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,
        form_struct_name = StructType::Form.format(table.struct_name.as_str()),
        item_id_params = item_id_params,
        item_id_filters = item_id_filters,
        tsync = if table_config.get_tsync() {
            "#[tsync::tsync]"
        } else {
            ""
        }
    )
}

fn build_imports(table: &ParsedTableMacro, config: &GenerationConfig) -> String {
    let belongs_imports = table
        .foreign_keys
        .iter()
        .map(|fk| {
            format!(
                "use crate::models::{foreign_table_name_model}::{singular_struct_name};",
                foreign_table_name_model = fk.0.to_string().to_snake_case().to_lowercase(),
                singular_struct_name = fk.0.to_string().to_pascal_case().to_singular()
            )
        })
        .collect::<Vec<String>>()
        .join("\n");

    format!(
        indoc! {"
        use crate::diesel::*;
        use crate::schema::*;
        use diesel::QueryResult;
        use serde::{{Deserialize, Serialize}};
        {belongs_imports}

        type Connection = {connection_type};
    "},
        connection_type = config.connection_type,
        belongs_imports = belongs_imports,
    )
}

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(&table, config);

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