use crate::generator::{GeneratedFile, Generator};
use crate::generators::field::Field;
use crate::generators::{to_pascal, to_snake, to_table_name};
use chrono::Utc;
use doido_core::Result;
const MIGRATION_LIB_BASE: &str = include_str!("../../templates/new/db/migration/src/lib.rs");
const MODELS_MOD_BASE: &str = include_str!("../../templates/new/app/models/mod.rs");
const MIGRATION_SRC_DIR: &str = "db/migration/src";
const MODELS_MOD_PATH: &str = "app/models/mod.rs";
pub struct ModelGenerator;
impl Generator for ModelGenerator {
fn name(&self) -> &str {
"model"
}
fn generate(&self, args: &[&str]) -> Result<Vec<GeneratedFile>> {
let name = args.first().copied().ok_or_else(|| {
doido_core::anyhow::anyhow!("model generator requires a name argument")
})?;
let snake = to_snake(name);
let table_name = to_table_name(name);
let fields = Field::parse_all(&args[1..])?;
let model = crate::templates::get("models/model.rs.template")
.replace("{table_name}", &table_name)
.replace("{fields}", &model_fields(&fields));
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
let migration_module = format!("m{timestamp}_create_{table_name}_table");
let migration = crate::templates::get("models/migration.rs.template")
.replace("{migration_imports}", &migration_imports(&fields))
.replace("{up_body}", &migration_up_body(&table_name, &fields))
.replace("{table_name}", &table_name);
let lib_path = format!("{MIGRATION_SRC_DIR}/lib.rs");
let existing =
std::fs::read_to_string(&lib_path).unwrap_or_else(|_| MIGRATION_LIB_BASE.to_string());
let lib = register_migration(&existing, &migration_module);
let models_mod_existing = std::fs::read_to_string(MODELS_MOD_PATH)
.unwrap_or_else(|_| MODELS_MOD_BASE.to_string());
let models_mod = register_model_module(&models_mod_existing, &snake);
let model_test = crate::templates::get("models/model_test.rs.template")
.replace("{Model}", &to_pascal(name))
.replace("{singular}", &snake);
Ok(vec![
GeneratedFile {
path: format!("app/models/{snake}.rs"),
content: model,
},
GeneratedFile {
path: format!("{MIGRATION_SRC_DIR}/{migration_module}.rs"),
content: migration,
},
GeneratedFile {
path: lib_path,
content: lib,
},
GeneratedFile {
path: MODELS_MOD_PATH.to_string(),
content: models_mod,
},
GeneratedFile {
path: format!("tests/{snake}_model_test.rs"),
content: model_test,
},
])
}
}
fn model_fields(fields: &[Field]) -> String {
fields
.iter()
.map(|f| format!(" {}\n", f.model_field()))
.collect()
}
fn migration_imports(fields: &[Field]) -> String {
if fields.iter().any(Field::wants_index) {
"use doido_model::migration::{add_index, create_table, drop_table};".to_string()
} else {
"use doido_model::migration::{create_table, drop_table};".to_string()
}
}
fn migration_up_body(table_name: &str, fields: &[Field]) -> String {
if fields.is_empty() {
return format!(
" // `create_table` adds an auto-incrementing `id` primary key for you.\n\
\x20 // Add columns with the builder, e.g. `t.string(\"name\").not_null();`.\n\
\x20 create_table(manager, \"{table_name}\", |_t| {{}}).await\n"
);
}
let columns: String = fields
.iter()
.map(|f| format!(" {}\n", f.migration_line()))
.collect();
let indexes: Vec<&Field> = fields.iter().filter(|f| f.wants_index()).collect();
let mut body = String::new();
body.push_str(
" // `create_table` adds an auto-incrementing `id` primary key for you.\n",
);
body.push_str(&format!(
" create_table(manager, \"{table_name}\", |t| {{\n{columns} }})\n"
));
if indexes.is_empty() {
body.push_str(" .await\n");
} else {
body.push_str(" .await?;\n");
for f in indexes {
body.push_str(&format!(
" add_index(manager, \"{table_name}\", &[\"{}\"]).await?;\n",
f.column_name()
));
}
body.push_str(" Ok(())\n");
}
body
}
fn register_model_module(models_mod: &str, module: &str) -> String {
let decl = format!("pub mod {module};");
if models_mod.lines().any(|l| l.trim() == decl) {
return models_mod.to_string();
}
let mut lines: Vec<String> = models_mod.lines().map(String::from).collect();
if let Some(i) = lines.iter().position(|l| l.contains("@generated-models")) {
lines.insert(i, decl);
} else {
lines.push(decl);
}
let mut out = lines.join("\n");
out.push('\n');
out
}
fn register_migration(lib: &str, module: &str) -> String {
let mut lines: Vec<String> = lib.lines().map(String::from).collect();
if let Some(i) = lines
.iter()
.position(|l| l.contains("@generated-migrations-mod"))
{
lines.insert(i, format!("mod {module};"));
}
if let Some(i) = lines
.iter()
.position(|l| l.contains("@generated-migrations-list"))
{
let indent: String = lines[i].chars().take_while(|c| c.is_whitespace()).collect();
lines.insert(i, format!("{indent}Box::new({module}::Migration),"));
}
let mut out = lines.join("\n");
out.push('\n');
out
}