use crate::generator::context::{self, markers};
use crate::relation;
use crate::template::TemplateEngine;
use crate::utils;
use anyhow::Result;
use heck::{ToPascalCase, ToSnakeCase};
use std::path::Path;
use tera::Context;
pub fn generate(
source_entity: &str,
target_entity: &str,
) -> Result<()> {
let engine = TemplateEngine::new()?;
let junction = relation::junction_name(source_entity, target_entity);
let junction_snake = junction.to_snake_case();
let base = Path::new("backend/src");
let project_root = Path::new(".");
if !relation::entity_exists(project_root, target_entity) {
println!(
" Target entity '{}' not found — storing pending M2M relation",
target_entity
);
relation::store_pending(
project_root,
relation::PendingRelation {
source_entity: source_entity.to_string(),
target_entity: target_entity.to_string(),
relation_type: "ManyToMany".to_string(),
},
)?;
return Ok(());
}
let junction_path = base.join(format!("entities/{}.rs", junction_snake));
if junction_path.exists() {
println!(" Junction entity '{}' already exists, skipping", junction);
inject_m2m_into_entity(&engine, source_entity, target_entity, &junction)?;
if relation::entity_exists(project_root, target_entity) {
inject_m2m_into_entity(&engine, target_entity, source_entity, &junction)?;
}
return Ok(());
}
let (entity_a, entity_b) = if source_entity.to_snake_case() < target_entity.to_snake_case() {
(source_entity, target_entity)
} else {
(target_entity, source_entity)
};
let entity_a_snake = entity_a.to_snake_case();
let entity_b_snake = entity_b.to_snake_case();
let mut ctx = Context::new();
ctx.insert("junction_table", &format!("{}_{}", entity_a_snake, entity_b_snake));
ctx.insert("junction_snake", &junction_snake);
ctx.insert("junction_iden", &junction.to_pascal_case());
ctx.insert("entity_a", &entity_a.to_pascal_case());
ctx.insert("entity_b", &entity_b.to_pascal_case());
ctx.insert("entity_a_snake", &entity_a_snake);
ctx.insert("entity_b_snake", &entity_b_snake);
ctx.insert("entity_a_table", &utils::pluralize(&entity_a_snake));
ctx.insert("entity_b_table", &utils::pluralize(&entity_b_snake));
let model_content = engine.render("entity/backend/junction_model.rs.tera", &ctx)?;
utils::write_file(&junction_path, &model_content)?;
let entities_mod = base.join("entities/mod.rs");
utils::insert_at_marker(
&entities_mod,
markers::MODS,
&format!("pub mod {};", junction_snake),
)?;
let timestamp = super::migration::next_timestamp();
let migration_content = engine.render("entity/backend/junction_migration.rs.tera", &ctx)?;
let migration_module = format!("m{}_create_{}_table", timestamp, junction_snake);
let migration_path =
Path::new("backend/migration/src").join(format!("{}.rs", migration_module));
utils::write_file(&migration_path, &migration_content)?;
context::register_migration(Path::new("."), &migration_module)?;
inject_m2m_into_entity(&engine, source_entity, target_entity, &junction)?;
if relation::entity_exists(project_root, target_entity) {
inject_m2m_into_entity(&engine, target_entity, source_entity, &junction)?;
}
println!(
" Generated M2M junction: {} <-> {} (via {})",
source_entity, target_entity, junction
);
Ok(())
}
const TEMPLATE_SPLIT_DELIMITER: &str = "\n---ROMANCE_SPLIT---\n";
fn inject_m2m_into_entity(
engine: &TemplateEngine,
entity: &str,
related: &str,
junction: &str,
) -> Result<()> {
let base = Path::new("backend/src");
let entity_snake = entity.to_snake_case();
let related_snake = related.to_snake_case();
let junction_snake = junction.to_snake_case();
let model_path = base.join(format!("entities/{}.rs", entity_snake));
if model_path.exists() {
let related_impl = format!(
r#"impl Related<super::{}::Entity> for Entity {{
fn to() -> RelationDef {{
super::{}::Relation::{}.def()
}}
fn via() -> Option<RelationDef> {{
Some(super::{}::Relation::{}.def().rev())
}}
}}"#,
related_snake,
junction_snake,
related.to_pascal_case(),
junction_snake,
entity.to_pascal_case(),
);
utils::insert_at_marker(
&model_path,
markers::RELATIONS,
&related_impl,
)?;
}
let handlers_path = base.join(format!("handlers/{}.rs", entity_snake));
if handlers_path.exists() {
let related_plural = utils::pluralize(&related_snake);
let mut ctx = Context::new();
ctx.insert("entity_name", &entity.to_pascal_case());
ctx.insert("entity_snake", &entity_snake);
ctx.insert("related_name", &related.to_pascal_case());
ctx.insert("related_snake", &related_snake);
ctx.insert("related_plural", &related_plural);
ctx.insert("junction_snake", &junction_snake);
let rendered = engine.render("entity/backend/m2m_handlers.rs.tera", &ctx)?;
let parts: Vec<&str> = rendered.split(TEMPLATE_SPLIT_DELIMITER).collect();
for part in &parts {
let trimmed = part.trim_end_matches('\n');
utils::insert_at_marker(
&handlers_path,
markers::RELATION_HANDLERS,
trimmed,
)?;
}
}
let routes_path = base.join(format!("routes/{}.rs", entity_snake));
if routes_path.exists() {
let project_root = Path::new(".");
let config = crate::config::RomanceConfig::load(project_root).ok();
let api_prefix = config.as_ref()
.and_then(|c| c.backend.api_prefix.clone())
.unwrap_or_else(|| "/api".to_string());
let entity_plural = utils::pluralize(&entity_snake);
let related_plural = utils::pluralize(&related_snake);
let mut ctx = Context::new();
ctx.insert("api_prefix", &api_prefix);
ctx.insert("entity_snake", &entity_snake);
ctx.insert("entity_plural", &entity_plural);
ctx.insert("related_snake", &related_snake);
ctx.insert("related_plural", &related_plural);
ctx.insert("related_id_param", &format!("{{{}_id}}", related_snake));
let rendered = engine.render("entity/backend/m2m_routes.rs.tera", &ctx)?;
let parts: Vec<&str> = rendered.split(TEMPLATE_SPLIT_DELIMITER).collect();
for part in &parts {
let trimmed = part.trim_end_matches('\n');
utils::insert_at_marker(
&routes_path,
markers::RELATION_ROUTES,
trimmed,
)?;
}
}
Ok(())
}