use doido_generators::{
default_registry, ChannelGenerator, ControllerGenerator, Generator, JobGenerator,
MailerGenerator, MigrationGenerator, ModelGenerator, ScaffoldGenerator,
};
#[test]
fn test_controller_generator_produces_correct_file() {
let files = ControllerGenerator.generate(&["Posts"]).unwrap();
let ctrl = files
.iter()
.find(|f| f.path == "src/controllers/posts_controller.rs")
.unwrap();
assert!(ctrl.content.contains("PostsController"));
assert!(ctrl.content.contains("#[controller]"));
let test = files
.iter()
.find(|f| f.path == "tests/posts_controller_test.rs")
.expect("controller test stub emitted");
assert!(test.content.contains("fn posts_controller_todo()"));
}
#[test]
fn test_model_generator_produces_model_migration_and_updates_lib() {
let files = ModelGenerator.generate(&["User"]).unwrap();
let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect();
assert!(paths.contains(&"app/models/user.rs"));
let model = files
.iter()
.find(|f| f.path == "app/models/user.rs")
.unwrap();
assert!(model.content.contains("DeriveEntityModel"));
assert!(model.content.contains("table_name = \"users\""));
assert!(paths
.iter()
.any(|p| p.starts_with("db/migration/src/m") && p.ends_with("_create_users_table.rs")));
let lib = files
.iter()
.find(|f| f.path == "db/migration/src/lib.rs")
.unwrap();
assert!(lib.content.contains("mod m"));
assert!(lib.content.contains("_create_users_table;"));
assert!(lib.content.contains("Box::new(m"));
assert!(lib.content.contains("_create_users_table::Migration),"));
}
#[test]
fn test_model_generator_pluralizes_irregular_table_name() {
let files = ModelGenerator.generate(&["Person"]).unwrap();
let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect();
assert!(paths.contains(&"app/models/person.rs"));
let model = files
.iter()
.find(|f| f.path == "app/models/person.rs")
.unwrap();
assert!(model.content.contains("table_name = \"people\""));
assert!(paths.iter().any(|p| p.ends_with("_create_people_table.rs")));
}
#[test]
fn test_model_generator_emits_columns_from_field_specs() {
let files = ModelGenerator
.generate(&[
"Post",
"title:string:not_null",
"body:text",
"author:references",
"slug:string:unique",
"views:integer:index",
])
.unwrap();
let model = files
.iter()
.find(|f| f.path == "app/models/post.rs")
.unwrap();
assert!(model.content.contains("pub title: String,"));
assert!(model.content.contains("pub body: Option<String>,"));
assert!(model.content.contains("pub author_id: i64,"));
assert!(model.content.contains("pub slug: Option<String>,"));
assert!(model.content.contains("pub views: Option<i32>,"));
let migration = files
.iter()
.find(|f| f.path.ends_with("_create_posts_table.rs"))
.unwrap();
assert!(migration
.content
.contains("t.string(\"title\").not_null();"));
assert!(migration.content.contains("t.text(\"body\");"));
assert!(migration.content.contains("t.references(\"author\");"));
assert!(migration
.content
.contains("t.string(\"slug\").unique_key();"));
assert!(migration.content.contains("t.integer(\"views\");"));
assert!(migration
.content
.contains("use doido_model::migration::{add_index, create_table, drop_table};"));
assert!(migration
.content
.contains("add_index(manager, \"posts\", &[\"views\"]).await?;"));
}
#[test]
fn test_model_generator_without_fields_emits_empty_table() {
let files = ModelGenerator.generate(&["User"]).unwrap();
let migration = files
.iter()
.find(|f| f.path.ends_with("_create_users_table.rs"))
.unwrap();
assert!(migration.content.contains("|_t| {}).await"));
assert!(migration
.content
.contains("use doido_model::migration::{create_table, drop_table};"));
assert!(!migration.content.contains("add_index"));
}
#[test]
fn test_model_generator_rejects_bad_field_type() {
assert!(ModelGenerator.generate(&["User", "age:notatype"]).is_err());
}
#[test]
fn test_model_generator_emits_test_stub() {
let files = ModelGenerator.generate(&["User"]).unwrap();
let test = files
.iter()
.find(|f| f.path == "tests/user_model_test.rs")
.expect("model test stub emitted");
assert!(test.content.contains("TODO"));
assert!(test.content.contains("fn user_model_todo()"));
}
#[test]
fn test_scaffold_emits_model_test_and_controller_request_tests() {
let files = ScaffoldGenerator
.generate(&["Post", "title:string"])
.unwrap();
let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect();
assert!(paths.contains(&"tests/post_model_test.rs"));
let ctrl = files
.iter()
.find(|f| f.path == "app/controllers/posts_controller.rs")
.unwrap();
assert!(!ctrl.content.contains("#[cfg(test)]"));
let test = files
.iter()
.find(|f| f.path == "tests/posts_controller_test.rs")
.expect("controller test file emitted");
assert!(test.content.contains("#[path = \"../config/routes.rs\"]")); assert!(test.content.contains("pool::test_lock()")); assert!(test.content.contains("title=Test")); for action in [
"index_request",
"new_request",
"create_request",
"show_request",
"edit_request",
"update_request",
"destroy_request",
] {
assert!(
test.content.contains(&format!("async fn {action}()")),
"missing per-action test {action}"
);
}
assert!(test.content.contains("StatusCode::OK"));
assert!(test.content.contains("StatusCode::FOUND"));
}
#[test]
fn test_migration_generator_create_pattern_uses_doido_model() {
let files = MigrationGenerator
.generate(&["create_users", "email:string:unique"])
.unwrap();
let mig = files
.iter()
.find(|f| f.path.starts_with("db/migration/src/") && f.path.ends_with("_create_users.rs"))
.unwrap();
assert!(mig
.content
.contains("create_table(manager, \"users\", |t| {"));
assert!(mig.content.contains("t.string(\"email\").unique_key();"));
assert!(mig.content.contains("drop_table(manager, \"users\").await"));
assert!(mig
.content
.contains("use doido_model::migration::{create_table, drop_table};"));
assert!(!mig.content.contains("Table::create()"));
assert!(!mig.content.contains("ColumnDef"));
let lib = files
.iter()
.find(|f| f.path == "db/migration/src/lib.rs")
.unwrap();
assert!(lib.content.contains("_create_users::Migration)"));
assert!(files
.iter()
.any(|f| f.path == "tests/create_users_migration_test.rs"));
}
#[test]
fn test_migration_generator_add_columns_pattern() {
let files = MigrationGenerator
.generate(&["add_slug_to_posts", "slug:string:unique"])
.unwrap();
let mig = files
.iter()
.find(|f| f.path.ends_with("_add_slug_to_posts.rs"))
.unwrap();
assert!(mig
.content
.contains("use doido_model::migration::alter_table;"));
assert!(mig
.content
.contains("alter_table(manager, \"posts\", |t| {"));
assert!(mig
.content
.contains("t.add_column(\"slug\", |c| { c.string().unique_key(); });"));
assert!(mig.content.contains("t.drop_column(\"slug\");"));
}
#[test]
fn test_migration_generator_remove_columns_pattern() {
let files = MigrationGenerator
.generate(&["remove_slug_from_posts", "slug:string"])
.unwrap();
let mig = files
.iter()
.find(|f| f.path.ends_with("_remove_slug_from_posts.rs"))
.unwrap();
assert!(mig.content.contains("t.drop_column(\"slug\");"));
assert!(mig
.content
.contains("t.add_column(\"slug\", |c| { c.string(); });"));
}
#[test]
fn test_migration_generator_generic_name_uses_doido_model_skeleton() {
let files = MigrationGenerator.generate(&["backfill_stuff"]).unwrap();
let mig = files
.iter()
.find(|f| f.path.ends_with("_backfill_stuff.rs"))
.unwrap();
assert!(mig
.content
.contains("create_table(manager, \"backfill_stuff\", |_t| {})"));
assert!(!mig.content.contains("Table::create()"));
assert!(!mig.content.contains("ColumnDef"));
assert!(!mig.content.contains("Alias::new"));
}
#[test]
fn test_job_generator_produces_correct_file() {
let files = JobGenerator.generate(&["SendEmail"]).unwrap();
let job = files
.iter()
.find(|f| f.path == "app/jobs/send_email_job.rs")
.unwrap();
assert!(job.content.contains("#[job"));
assert!(job
.content
.contains("async fn send_email_job(ctx: &JobContext, payload: SendEmailPayload)"));
assert!(job.content.contains("pub struct SendEmailPayload"));
assert!(job.content.contains("Serialize"));
assert!(job.content.contains("Deserialize"));
let test = files
.iter()
.find(|f| f.path == "tests/send_email_job_test.rs")
.expect("job test stub emitted");
assert!(test.content.contains("fn send_email_job_todo()"));
}
#[test]
fn test_mailer_generator_produces_correct_file() {
let files = MailerGenerator.generate(&["Welcome"]).unwrap();
let mailer = files
.iter()
.find(|f| f.path == "app/mailers/welcome_mailer.rs")
.unwrap();
assert!(mailer.content.contains("WelcomeMailer"));
assert!(files
.iter()
.any(|f| f.path == "tests/welcome_mailer_test.rs"));
}
#[test]
fn test_channel_generator_produces_correct_file() {
let files = ChannelGenerator.generate(&["Chat"]).unwrap();
let channel = files
.iter()
.find(|f| f.path == "app/channels/chat_channel.rs")
.unwrap();
assert!(channel.content.contains("ChatChannel"));
assert!(files.iter().any(|f| f.path == "tests/chat_channel_test.rs"));
}
#[test]
fn test_scaffold_generator_produces_multiple_files() {
let files = ScaffoldGenerator
.generate(&["Post", "title:string:not_null", "body:text"])
.unwrap();
let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect();
assert!(paths.contains(&"app/models/post.rs"));
assert!(paths.contains(&"app/controllers/posts_controller.rs"));
assert!(paths.iter().any(|p| p.ends_with("_create_posts_table.rs")));
assert!(paths.contains(&"app/views/posts/index.html.tera"));
assert!(paths.contains(&"app/views/posts/show.html.tera"));
assert!(paths.contains(&"app/views/posts/new.html.tera"));
assert!(paths.contains(&"app/views/posts/edit.html.tera"));
assert!(paths.contains(&"app/views/posts/_form.html.tera"));
assert!(paths.contains(&"config/routes.rs"));
let ctrl = files
.iter()
.find(|f| f.path == "app/controllers/posts_controller.rs")
.unwrap();
for action in [
"index", "show", "new", "create", "edit", "update", "destroy",
] {
assert!(
ctrl.content.contains(&format!("fn {action}(")),
"missing action {action}"
);
}
assert!(ctrl.content.contains("pub struct PostsController;"));
assert!(ctrl.content.contains("pub struct PostForm"));
assert!(ctrl.content.contains("pub title: String,"));
assert!(ctrl.content.contains("title: Set(form.title),"));
assert!(ctrl.content.contains("ctx.render(\"posts/index\""));
let routes = files.iter().find(|f| f.path == "config/routes.rs").unwrap();
assert!(routes
.content
.contains("resources!(posts, PostsController);"));
assert!(routes
.content
.contains("use crate::controllers::PostsController;"));
assert!(routes.content.contains("HelloController::index"));
let cmod = files
.iter()
.find(|f| f.path == "app/controllers/mod.rs")
.unwrap();
assert!(cmod.content.contains("mod posts_controller;"));
assert!(cmod
.content
.contains("pub use posts_controller::PostsController;"));
let index = files
.iter()
.find(|f| f.path == "app/views/posts/index.html.tera")
.unwrap();
assert!(index.content.contains("<th>title</th>"));
assert!(index.content.contains("{% for post in posts %}"));
assert!(index.content.contains("{{ post.title }}"));
assert!(index.content.contains(">Delete</button>"));
assert!(index
.content
.contains("fetch('/posts/{{ post.id }}', { method: 'DELETE' })"));
}
#[test]
fn test_scaffold_api_emits_json_controller_and_no_views() {
let files = ScaffoldGenerator
.generate(&["Post", "title:string", "--api"])
.unwrap();
let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect();
assert!(!paths.iter().any(|p| p.contains("app/views/")));
let ctrl = files
.iter()
.find(|f| f.path == "app/controllers/posts_controller.rs")
.unwrap();
assert!(ctrl.content.contains("ctx.json("));
assert!(ctrl.content.contains("ctx.body_json()"));
assert!(!ctrl.content.contains("ctx.render("));
let routes = files.iter().find(|f| f.path == "config/routes.rs").unwrap();
assert!(routes
.content
.contains("resources!(posts, PostsController, except: [new, edit]);"));
}
#[test]
fn test_default_registry_has_all_generators() {
let reg = default_registry();
let names = reg.list();
assert!(names.contains(&"controller"));
assert!(names.contains(&"model"));
assert!(names.contains(&"migration"));
assert!(names.contains(&"job"));
assert!(names.contains(&"mailer"));
assert!(names.contains(&"channel"));
assert!(names.contains(&"scaffold"));
}
#[test]
fn test_registry_runs_generator_by_name() {
let reg = default_registry();
let files = reg.run("controller", &["Admin"]).unwrap();
assert_eq!(files[0].path, "src/controllers/admin_controller.rs");
}
#[test]
fn test_generator_missing_arg_returns_error() {
let result = ControllerGenerator.generate(&[]);
assert!(result.is_err());
}
#[test]
fn test_registry_unknown_generator_returns_error() {
let reg = default_registry();
assert!(reg.run("nonexistent", &[]).is_err());
}
use doido_generators::{project_generator, templates, GeneratorGenerator, TemplatesGenerator};
use std::fs;
use tempfile::tempdir;
#[test]
fn test_resolve_prefers_project_override() {
let dir = tempdir().unwrap();
let rel = "controller/controller.rs.template";
let default = templates::get_with_root(dir.path(), rel);
assert!(default.contains("{pascal}Controller"));
fs::create_dir_all(dir.path().join("controller")).unwrap();
fs::write(dir.path().join(rel), "OVERRIDDEN {pascal}").unwrap();
let overridden = templates::get_with_root(dir.path(), rel);
assert_eq!(overridden, "OVERRIDDEN {pascal}");
}
#[test]
fn test_templates_generator_ejects_all_and_filtered() {
let all = TemplatesGenerator.generate(&[]).unwrap();
assert!(all
.iter()
.any(|f| f.path == "templates/scaffold/views/index.html.tera"));
assert!(all
.iter()
.any(|f| f.path == "templates/controller/controller.rs.template"));
let scaffold = TemplatesGenerator.generate(&["scaffold"]).unwrap();
assert!(scaffold
.iter()
.all(|f| f.path.starts_with("templates/scaffold/")));
assert!(scaffold.len() < all.len());
assert!(TemplatesGenerator.generate(&["bogus"]).is_err());
}
#[test]
fn test_generator_generator_scaffolds_project_generator() {
let files = GeneratorGenerator.generate(&["Widget"]).unwrap();
assert!(files
.iter()
.any(|f| f.path == "lib/generators/widget/app/widgets/{snake}.rs.template"));
assert!(files
.iter()
.any(|f| f.path == "lib/generators/widget/README.md"));
}
#[test]
fn test_project_generator_substitutes_path_and_content() {
let root = tempdir().unwrap();
let gen_dir = root.path().join("widget");
fs::create_dir_all(gen_dir.join("app/{plural}")).unwrap();
fs::write(
gen_dir.join("app/{plural}/{snake}.rs.template"),
"pub struct {pascal}; // {plural}\n",
)
.unwrap();
fs::write(gen_dir.join("README.md"), "ignored").unwrap();
fs::write(root.path().join("notes.txt"), "x").unwrap();
assert_eq!(
project_generator::list_in(root.path()),
vec!["widget".to_string()]
);
let dir = project_generator::find_in(root.path(), "widget").unwrap();
let files = project_generator::run(&dir, &["BlogPost"]).unwrap();
assert_eq!(files.len(), 1); let f = &files[0];
assert_eq!(f.path, "app/blog_posts/blog_post.rs"); assert_eq!(f.content, "pub struct BlogPost; // blog_posts\n");
}