use indoc::indoc;
use inflector::Inflector;
use crate::GenerationConfig;
use crate::parser::{FILE_SIGNATURE, ParsedTableMacro};
#[derive(PartialEq, Eq)]
enum StructType {
Form,
Read,
}
impl StructType {
pub fn prefix(&self) -> &'static str {
match self {
StructType::Form => "",
StructType::Read => "",
}
}
pub fn suffix(&self) -> &'static str {
match self {
StructType::Form => "Form",
StructType::Read => "",
}
}
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(), 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_name} {{
$COLUMNS$
}}
"#},
tsync_attr = if table_options.get_tsync() { "#[tsync::tsync]\n" } else { "" },
table_name = table.name.to_string(),
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 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();
if ty != StructType::Read && table_options.get_autogenerated_columns().contains(&field_name.as_str()) {
continue;
}
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;
}
if col.is_nullable {
field_type = format!("Option<{}>", col.ty.to_string());
}
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(config: &GenerationConfig) -> String {
format!(indoc! {"
use crate::diesel::*;
use crate::schema::*;
use diesel::QueryResult;
use serde::{{Deserialize, Serialize}};
type Connection = {connection_type};
"},
connection_type = config.connection_type
)
}
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}")
}