use crate::admin::daemon::parser::{ParsedAdmin, ResourceDef};
use crate::utils::trad::t;
use std::{fmt::Write, fs, path::Path};
pub(crate) fn generate(parsed: &ParsedAdmin) -> Result<(), String> {
let admins_dir = Path::new("src/admins");
fs::create_dir_all(admins_dir)
.map_err(|e| format!("Impossible to create {}: {}", admins_dir.display(), e))?;
write_readme(admins_dir)?;
write_admin(parsed, admins_dir)?;
write_mod(admins_dir)?;
Ok(())
}
fn write_readme(dir: &Path) -> Result<(), String> {
let content = format!(
"<!-- {}\n {} -->\n",
t("daemon.generated_header"),
t("daemon.generated_notice")
);
fs::write(dir.join("README.md"), content)
.map_err(|e| format!("Impossible to write README.md: {}", e))
}
fn write_mod(dir: &Path) -> Result<(), String> {
let content = "pub mod admin;\npub use admin::{routes, admin_state};\n";
fs::write(dir.join("mod.rs"), content).map_err(|e| format!("Impossible to write mod.rs: {}", e))
}
fn write_admin(parsed: &ParsedAdmin, dir: &Path) -> Result<(), String> {
let resources = &parsed.resources;
let mut out = String::new();
let _ = writeln!(out, "// {}", t("daemon.generated_header"));
let _ = writeln!(out, "// {}", t("daemon.generated_notice"));
let _ = writeln!(out);
let _ = writeln!(out, "use runique::prelude::*;");
let _ = writeln!(out);
for r in resources {
let module = model_to_module(&r.model_type);
let _ = writeln!(out, "use crate::entities::{};", module);
}
let _ = writeln!(out);
for r in resources {
write_dyn_form_impl(&mut out, r)?;
if r.edit_form_type.is_some() {
write_edit_dyn_form_impl(&mut out, r)?;
} else if r.bulk_create.is_some() {
let module = model_to_module(&r.model_type);
let default_form = format!("{}::AdminForm", module);
let edit_wrapper = format!("{}EditFormDynWrapper", pascal_case(&module));
write_dyn_form_wrapper(
&mut out,
&format!(
"DynForm edit wrapper for {} (auto from bulk_create)",
module
),
&edit_wrapper,
&default_form,
"_db",
"Ok(()) // update_fn handles persistence",
)?;
}
}
write_admin_register(&mut out, parsed)?;
fs::write(dir.join("admin.rs"), out).map_err(|e| format!("Impossible to write admin.rs: {}", e))
}
fn write_dyn_form_impl(out: &mut String, r: &ResourceDef) -> Result<(), String> {
let module = model_to_module(&r.model_type);
let form_path = r
.create_form_type
.as_deref()
.unwrap_or(&format!("{}::AdminForm", module))
.to_string();
let wrapper = format!("{}AdminFormDynWrapper", pascal_case(&module));
write_dyn_form_wrapper(
out,
&format!("DynForm wrapper for {}::AdminForm", module),
&wrapper,
&form_path,
"db",
"self.0.save(db).await",
)
}
fn write_edit_dyn_form_impl(out: &mut String, r: &ResourceDef) -> Result<(), String> {
let edit_form_path = r.edit_form_type.as_ref().unwrap();
let module = model_to_module(&r.model_type);
let wrapper = format!("{}EditFormDynWrapper", pascal_case(&module));
write_dyn_form_wrapper(
out,
&format!("DynForm edit wrapper for {}", edit_form_path),
&wrapper,
edit_form_path,
"_db",
"Ok(()) // update_fn handles persistence",
)
}
fn write_admin_register(out: &mut String, parsed: &ParsedAdmin) -> Result<(), String> {
let resources = &parsed.resources;
let _ = writeln!(out, "/// Builds the admin registry at boot.");
let _ = writeln!(out, "/// Called by the application builder.");
let _ = writeln!(out, "pub fn admin_register() -> AdminRegistry {{");
let _ = writeln!(out, " let mut registry = AdminRegistry::new();");
let _ = writeln!(
out,
" for entry in runique::admin::builtin_resources() {{"
);
let _ = writeln!(out, " registry.register(entry);");
let _ = writeln!(out, " }}");
let _ = writeln!(out);
for r in resources {
write_resource_entry(out, r)?;
}
for cfg in &parsed.configures {
if cfg.hidden {
let _ = writeln!(out, " registry.remove(\"{}\");", cfg.key);
continue;
}
let has_display = !cfg.list_display.is_empty()
|| !cfg.list_exclude.is_empty()
|| !cfg.list_filter.is_empty();
if has_display {
let display_3: Vec<(
String,
String,
Option<crate::admin::daemon::parser::FkDisplay>,
)> = cfg
.list_display
.iter()
.map(|(c, l)| (c.clone(), l.clone(), None))
.collect();
let chain = build_display_chain(&display_3, &cfg.list_exclude, &cfg.list_filter);
let _ = writeln!(out, " registry.configure(\"{}\", {});", cfg.key, chain);
}
if !cfg.group_action.is_empty() {
let actions_str = build_group_actions_str(&cfg.group_action);
let _ = writeln!(
out,
" registry.configure_group_actions(\"{}\", vec![{}]);",
cfg.key, actions_str
);
}
}
let _ = writeln!(out, " registry");
let _ = writeln!(out, "}}");
let _ = writeln!(out);
let _ = writeln!(out, "/// Builds the admin CRUD routes for the given path.");
let _ = writeln!(
out,
"/// To be passed to `.with_admin(|a| a.routes(admins::routes(\"/admin\")))` in main.rs."
);
let _ = writeln!(
out,
"/// This is where the admin lives. To serve it behind an extra segment"
);
let _ = writeln!(
out,
"/// (`/secret/admin/…`), add `.prefix(\"secret\")` — the two are independent."
);
let _ = writeln!(
out,
"pub fn routes(path: &str) -> runique::admin::AdminRoutes {{"
);
let _ = writeln!(out, " let p = path.trim_end_matches('/');");
let _ = writeln!(out, " let router = runique::axum::Router::new()");
let _ = writeln!(
out,
" .route(&format!(\"{{}}/{{{{resource}}}}/{{{{action}}}}\", p), get(admin_get).post(admin_post))"
);
let _ = writeln!(
out,
" .route(&format!(\"{{}}/{{{{resource}}}}/{{{{id}}}}/{{{{action}}}}\", p), get(admin_get_id).post(admin_post_id))"
);
let _ = writeln!(
out,
" .route(&format!(\"{{}}/{{{{parent}}}}/{{{{parent_id}}}}/{{{{resource}}}}/{{{{action}}}}\", p), get(admin_nested_get).post(admin_nested_post))"
);
let _ = writeln!(
out,
" .route(&format!(\"{{}}/{{{{parent}}}}/{{{{parent_id}}}}/{{{{resource}}}}/{{{{id}}}}/{{{{action}}}}\", p), get(admin_nested_get_id).post(admin_nested_post_id));"
);
let _ = writeln!(out, " runique::admin::AdminRoutes::new(p, router)");
let _ = writeln!(out, "}}");
let _ = writeln!(out);
let _ = writeln!(
out,
"/// Returns the shared state of the admin prototype (for the dashboard)."
);
let _ = writeln!(
out,
"pub fn admin_state() -> std::sync::Arc<PrototypeAdminState> {{"
);
let _ = writeln!(out, " let config = Arc::new(AdminConfig::new());");
let _ = writeln!(out, " std::sync::Arc::new(PrototypeAdminState {{");
let _ = writeln!(out, " registry: Arc::new(admin_register()),");
let _ = writeln!(out, " config,");
let _ = writeln!(out, " }})");
let _ = writeln!(out, "}}");
Ok(())
}
fn write_resource_entry(out: &mut String, r: &ResourceDef) -> Result<(), String> {
let key = &r.key;
let model = full_model_path(&r.model_type);
let title = &r.title;
let module = model_to_module(&r.model_type);
let form_path = r
.create_form_type
.as_deref()
.unwrap_or(&format!("{}::AdminForm", module))
.to_string();
let wrapper = format!("{}AdminFormDynWrapper", pascal_case(&module));
let id_parse_code = match r.id_type.as_str() {
"I32" => {
"let id = id.parse::<i32>().map_err(|_| DbErr::Custom(\"invalid id\".to_string()))?"
}
"I64" => {
"let id = id.parse::<i64>().map_err(|_| DbErr::Custom(\"invalid id\".to_string()))?"
}
"Uuid" => {
"let id = uuid::Uuid::parse_str(&id).map_err(|_| DbErr::Custom(\"invalid id\".to_string()))?"
}
_ => {
"let id = id.parse::<Pk>().map_err(|_| DbErr::Custom(\"invalid id\".to_string()))?"
}
};
let id_parse_or_not_found = match r.id_type.as_str() {
"I32" => "let Ok(id) = id.parse::<i32>() else { return Ok(None) }",
"I64" => "let Ok(id) = id.parse::<i64>() else { return Ok(None) }",
"Uuid" => "let Ok(id) = uuid::Uuid::parse_str(&id) else { return Ok(None) }",
_ => "let Ok(id) = id.parse::<Pk>() else { return Ok(None) }",
};
let _ = writeln!(out, " // ── Resource: {} ──", key);
let _ = writeln!(out, " let meta = AdminResource::new(");
let _ = writeln!(out, " \"{}\",", key);
let _ = writeln!(out, " \"{}\",", model);
let _ = writeln!(out, " \"AdminForm\",");
let _ = writeln!(out, " \"{}\",", title);
let _ = writeln!(out, " vec![],");
let _ = writeln!(out, " );");
if r.create_form_type.is_some() {
let _ = writeln!(out, " let meta = meta.inject_password(true);");
}
if let Some(ref t) = r.template_list {
let _ = writeln!(out, " let meta = meta.template_list(\"{}\");", t);
}
if let Some(ref t) = r.template_create {
let _ = writeln!(out, " let meta = meta.template_create(\"{}\");", t);
}
if let Some(ref t) = r.template_edit {
let _ = writeln!(out, " let meta = meta.template_edit(\"{}\");", t);
}
if let Some(ref t) = r.template_detail {
let _ = writeln!(out, " let meta = meta.template_detail(\"{}\");", t);
}
if let Some(ref t) = r.template_delete {
let _ = writeln!(out, " let meta = meta.template_delete(\"{}\");", t);
}
for (k, v) in &r.extra_context {
let _ = writeln!(out, " let meta = meta.extra(\"{}\", \"{}\");", k, v);
}
let fk_cols_for_form: Vec<_> = r
.list_display
.iter()
.filter(|(_, _, fk)| fk.is_some())
.collect();
write_form_builder_closure_fk(out, "form_builder", &form_path, &wrapper, &fk_cols_for_form);
let _ = writeln!(
out,
" let list_fn: ListFn = Arc::new(|db: ADb, params: ListParams| {{"
);
let _ = writeln!(out, " Box::pin(async move {{");
let _ = writeln!(
out,
" use sea_orm::{{QueryFilter, sea_query::{{Alias, Expr, Order}}}};"
);
let _ = writeln!(
out,
" let mut query = {}::Entity::find();",
module
);
let sort_cols: Vec<String> = std::iter::once("id".to_string())
.chain(r.list_display.iter().map(|(col, _, _)| col.clone()))
.collect();
let sort_cols_literal = sort_cols
.iter()
.map(|c| format!("\"{}\"", c))
.collect::<Vec<_>>()
.join(", ");
let filter_cols: Vec<String> = r
.list_filter
.iter()
.map(|(col, _, _)| col.clone())
.collect();
let filter_cols_literal = filter_cols
.iter()
.map(|c| format!("\"{}\"", c))
.collect::<Vec<_>>()
.join(", ");
let _ = writeln!(
out,
" const SORT_COLS: &[&str] = &[{sort_cols_literal}];",
sort_cols_literal = sort_cols_literal
);
let _ = writeln!(
out,
" const FILTER_COLS: &[&str] = &[{filter_cols_literal}];",
filter_cols_literal = filter_cols_literal
);
let _ = writeln!(
out,
" if let Some(ref col) = params.sort_by && SORT_COLS.contains(&col.as_str()) {{"
);
let _ = writeln!(
out,
" let order = if params.sort_dir == SortDir::Desc {{ Order::Desc }} else {{ Order::Asc }};"
);
let _ = writeln!(
out,
" query = query.order_by(Expr::col(Alias::new(col.as_str())), order);"
);
let _ = writeln!(out, " }}");
let _ = writeln!(
out,
" for (col, val) in ¶ms.column_filters {{"
);
let _ = writeln!(
out,
" if !FILTER_COLS.contains(&col.as_str()) {{ continue; }}"
);
let _ = writeln!(
out,
" query = query.filter(text_eq(&db, col.as_str(), val));"
);
let _ = writeln!(out, " }}");
let _ = writeln!(
out,
" if let Some((col, val)) = ¶ms.scope && col.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {{"
);
let _ = writeln!(
out,
" query = query.filter(text_eq(&db, col.as_str(), val));"
);
let _ = writeln!(out, " }}");
let _ = writeln!(
out,
" if let Some(ref search_str) = params.search {{"
);
write_search_conditions(out, &r.list_display, &module);
let _ = writeln!(out, " query = query.filter(search_cond);");
let _ = writeln!(out, " }}");
let _ = writeln!(
out,
" let db_rows = query.offset(params.offset).limit(params.limit).all(&*db).await?;"
);
let _ = writeln!(
out,
" let rows: Vec<serde_json::Value> = db_rows.into_iter()"
);
let _ = writeln!(
out,
" .map(|r| serde_json::to_value(r).unwrap_or(serde_json::Value::Null))"
);
let _ = writeln!(out, " .collect();");
let fk_cols: Vec<_> = r
.list_display
.iter()
.filter(|(_, _, fk)| fk.is_some())
.collect();
let fk_display_specs: String = fk_cols
.iter()
.map(|(col, _, fk_opt)| {
let fk = fk_opt.as_ref().unwrap();
format!(
"({c:?}.to_string(), {t:?}.to_string(), {fc:?}.to_string())",
c = col,
t = fk.table,
fc = fk.col
)
})
.collect::<Vec<_>>()
.join(", ");
let _ = writeln!(out, " Ok(rows)");
let _ = writeln!(out, " }})");
let _ = writeln!(out, " }});");
let _ = writeln!(out);
let _ = writeln!(
out,
" let count_fn: CountFn = Arc::new(|db: ADb, _search: Option<String>, scope: Option<(String, String)>| {{"
);
let _ = writeln!(out, " Box::pin(async move {{");
let _ = writeln!(out, " use sea_orm::QueryFilter;");
let _ = writeln!(
out,
" let mut query = {}::Entity::find();",
module
);
let _ = writeln!(out, " if let Some(ref search_str) = _search {{");
write_search_conditions(out, &r.list_display, &module);
let _ = writeln!(out, " query = query.filter(search_cond);");
let _ = writeln!(out, " }}");
let _ = writeln!(
out,
" if let Some((col, val)) = &scope && col.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {{"
);
let _ = writeln!(
out,
" query = query.filter(text_eq(&db, col.as_str(), val));"
);
let _ = writeln!(out, " }}");
let _ = writeln!(out, " query.count(&*db).await");
let _ = writeln!(out, " }})");
let _ = writeln!(out, " }});");
let _ = writeln!(out);
let _ = writeln!(
out,
" let get_fn: GetFn = Arc::new(|db: ADb, id: String| {{"
);
let _ = writeln!(out, " Box::pin(async move {{");
let _ = writeln!(out, " {};", id_parse_or_not_found);
let _ = writeln!(
out,
" let row = {}::Entity::find_by_id(id).one(&*db).await?;",
module
);
let _ = writeln!(
out,
" Ok(row.map(|r| serde_json::to_value(r).unwrap_or(serde_json::Value::Null)))"
);
let _ = writeln!(out, " }})");
let _ = writeln!(out, " }});");
let _ = writeln!(out);
let _ = writeln!(
out,
" let delete_fn: DeleteFn = Arc::new(|db: ADb, id: String| {{"
);
let _ = writeln!(out, " Box::pin(async move {{");
let _ = writeln!(out, " {};", id_parse_code);
let _ = writeln!(
out,
" {}::Entity::delete_by_id(id).exec(&*db).await.map(|_| ())",
module
);
let _ = writeln!(out, " }})");
let _ = writeln!(out, " }});");
let _ = writeln!(out);
let _ = writeln!(
out,
" let create_fn: CreateFn = Arc::new(|db: ADb, data: StrMap| {{"
);
let _ = writeln!(out, " Box::pin(async move {{");
if let Some(ref bulk_field) = r.bulk_create {
let _ = writeln!(out, " use sea_orm::QueryFilter;");
let _ = writeln!(
out,
" let raw = data.get(\"{bulk_field}\").cloned().unwrap_or_default();",
bulk_field = bulk_field
);
let _ = writeln!(
out,
" let values: Vec<&str> = raw.split(',').map(str::trim).filter(|v| !v.is_empty()).collect();"
);
let _ = writeln!(out, " for val in values {{");
let _ = writeln!(out, " let mut row = data.clone();");
let _ = writeln!(
out,
" row.insert(\"{bulk_field}\".to_string(), val.to_string());",
bulk_field = bulk_field
);
let _ = writeln!(
out,
" let existing_id: Option<String> = {}::Entity::find()",
module
);
let _ = writeln!(
out,
" .filter(text_eq(&db, \"{bulk_field}\", val))",
bulk_field = bulk_field
);
let _ = writeln!(out, " .one(&*db).await?");
let _ = writeln!(out, " .map(|m| m.id.to_string());");
let _ = writeln!(out, " if let Some(id) = existing_id {{");
let _ = writeln!(out, " {};", id_parse_code);
let _ = writeln!(
out,
" {}::admin_from_form(&row, Some(id)).update(&*db).await?;",
module
);
let _ = writeln!(out, " }} else {{");
let _ = writeln!(
out,
" {}::admin_from_form(&row, None).insert(&*db).await?;",
module
);
let _ = writeln!(out, " }}");
let _ = writeln!(out, " }}");
let _ = writeln!(out, " Ok(())");
} else if r.m2m.is_empty() {
let _ = writeln!(out, " {}::admin_from_form(&data, None)", module);
let _ = writeln!(out, " .insert(&*db).await.map(|_| ())");
} else {
let _ = writeln!(out, " use sea_orm::ConnectionTrait;");
let _ = writeln!(
out,
" let result = {}::admin_from_form(&data, None).insert(&*db).await?;",
module
);
let _ = writeln!(out, " let inserted_id = result.id.to_string();");
for m2m in &r.m2m {
let prefix = format!("m2m_{}__", m2m.field_name);
let _ = writeln!(out, " for key in data.keys() {{");
let _ = writeln!(
out,
" if let Some(target_id) = key.strip_prefix(\"{prefix}\") && !target_id.is_empty() && target_id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {{",
prefix = prefix
);
let _ = writeln!(
out,
" let sql = format!(\"INSERT INTO {junction} ({self_fk}, {target_fk}) VALUES ({{}}, {{}}) ON CONFLICT DO NOTHING\", inserted_id, target_id);",
junction = m2m.junction_table,
self_fk = m2m.self_fk,
target_fk = m2m.target_fk
);
let _ = writeln!(
out,
" let _ = db.execute_unprepared(&sql).await;"
);
let _ = writeln!(out, " }}");
let _ = writeln!(out, " }}");
}
let _ = writeln!(out, " Ok(())");
}
let _ = writeln!(out, " }})");
let _ = writeln!(out, " }});");
let _ = writeln!(out);
let _ = writeln!(
out,
" let update_fn: UpdateFn = Arc::new(|db: ADb, id: String, data: StrMap| {{"
);
let _ = writeln!(out, " Box::pin(async move {{");
let _ = writeln!(out, " {};", id_parse_code);
if r.m2m.is_empty() {
let _ = writeln!(
out,
" {}::admin_from_form(&data, Some(id))",
module
);
let _ = writeln!(out, " .update(&*db).await.map(|_| ())");
} else {
let _ = writeln!(out, " use sea_orm::ConnectionTrait;");
let _ = writeln!(out, " let id_str = id.to_string();");
let _ = writeln!(
out,
" {}::admin_from_form(&data, Some(id)).update(&*db).await?;",
module
);
for m2m in &r.m2m {
let prefix = format!("m2m_{}__", m2m.field_name);
let _ = writeln!(
out,
" let _ = db.execute_unprepared(&format!(\"DELETE FROM {junction} WHERE {self_fk} = {{}}\", id_str)).await;",
junction = m2m.junction_table,
self_fk = m2m.self_fk
);
let _ = writeln!(out, " for key in data.keys() {{");
let _ = writeln!(
out,
" if let Some(target_id) = key.strip_prefix(\"{prefix}\") && !target_id.is_empty() && target_id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {{",
prefix = prefix
);
let _ = writeln!(
out,
" let sql = format!(\"INSERT INTO {junction} ({self_fk}, {target_fk}) VALUES ({{}}, {{}})\", id_str, target_id);",
junction = m2m.junction_table,
self_fk = m2m.self_fk,
target_fk = m2m.target_fk
);
let _ = writeln!(
out,
" let _ = db.execute_unprepared(&sql).await;"
);
let _ = writeln!(out, " }}");
let _ = writeln!(out, " }}");
}
let _ = writeln!(out, " Ok(())");
}
let _ = writeln!(out, " }})");
let _ = writeln!(out, " }});");
let _ = writeln!(out);
let _ = writeln!(
out,
" let partial_update_fn: UpdateFn = Arc::new(|db: ADb, id: String, data: StrMap| {{"
);
let _ = writeln!(out, " Box::pin(async move {{");
let _ = writeln!(out, " {};", id_parse_code);
let _ = writeln!(
out,
" {}::admin_partial_update(&data, id)",
module
);
let _ = writeln!(out, " .update(&*db).await.map(|_| ())");
let _ = writeln!(out, " }})");
let _ = writeln!(out, " }});");
let _ = writeln!(out);
if !r.m2m.is_empty() {
let _ = writeln!(
out,
" let m2m_loader: M2mLoaderFn = Arc::new(|db: ADb, object_id: Option<String>| {{"
);
let _ = writeln!(out, " Box::pin(async move {{");
let _ = writeln!(
out,
" use sea_orm::{{EntityTrait, ConnectionTrait}};"
);
let _ = writeln!(
out,
" let mut fields: Vec<M2mFieldOptions> = Vec::new();"
);
for m2m in &r.m2m {
let _ = writeln!(out, " {{");
let _ = writeln!(
out,
" let rows = {entity}::Entity::find().all(&*db).await.unwrap_or_default();",
entity = m2m.target_entity
);
let _ = writeln!(
out,
" let choices: Vec<(String, String)> = rows.iter().map(|r| {{"
);
let _ = writeln!(
out,
" let v = serde_json::to_value(r).unwrap_or_default();"
);
let _ = writeln!(
out,
" let id = v.get(\"id\").map(|i| i.to_string().trim_matches('\"').to_string()).unwrap_or_default();"
);
let _ = writeln!(
out,
" let label = v.get(\"{display}\").and_then(|n| n.as_str()).unwrap_or(\"\").to_string();",
display = m2m.target_display
);
let _ = writeln!(out, " (id, label)");
let _ = writeln!(out, " }}).collect();");
let _ = writeln!(
out,
" let selected = if let Some(ref oid) = object_id {{"
);
let _ = writeln!(
out,
" use sea_orm::sea_query::{{Query, Alias, Expr}};"
);
let _ = writeln!(
out,
" let stmt = Query::select().expr(Expr::cust(format!(\"CAST({target_fk} AS {{}})\", text_cast_type(&db)))).from(Alias::new(\"{junction}\")).and_where(text_eq(&db, \"{self_fk}\", oid)).to_owned();",
target_fk = m2m.target_fk,
junction = m2m.junction_table,
self_fk = m2m.self_fk
);
let _ = writeln!(
out,
" db.query_all(&stmt).await.unwrap_or_default().iter().filter_map(|r| r.try_get_by_index::<String>(0).ok()).collect()"
);
let _ = writeln!(out, " }} else {{ vec![] }};");
let _ = writeln!(out, " fields.push(M2mFieldOptions {{");
let _ = writeln!(
out,
" field_name: \"{field_name}\".to_string(),",
field_name = m2m.field_name
);
let _ = writeln!(
out,
" label: \"{label}\".to_string(),",
label = m2m.label
);
let _ = writeln!(out, " choices,");
let _ = writeln!(out, " selected,");
let _ = writeln!(out, " }});");
let _ = writeln!(out, " }}");
}
let _ = writeln!(out, " fields");
let _ = writeln!(out, " }})");
let _ = writeln!(out, " }});");
let _ = writeln!(out);
}
if let Some(ref edit_form_path) = r.edit_form_type {
let edit_wrapper = format!("{}EditFormDynWrapper", pascal_case(&module));
write_form_builder_closure(out, "edit_form_builder", edit_form_path, &edit_wrapper);
} else if r.bulk_create.is_some() {
let default_form = format!("{}::AdminForm", module);
let edit_wrapper = format!("{}EditFormDynWrapper", pascal_case(&module));
write_form_builder_closure(out, "edit_form_builder", &default_form, &edit_wrapper);
}
if !r.list_display.is_empty() || !r.list_exclude.is_empty() || !r.list_filter.is_empty() {
let chain = build_display_chain(&r.list_display, &r.list_exclude, &r.list_filter);
let _ = writeln!(out, " let meta = meta.display({});", chain);
}
if !fk_display_specs.is_empty() {
let _ = writeln!(
out,
" let meta = meta.fk_display(vec![{fk_display_specs}]);"
);
}
if !r.list_filter.is_empty() {
let _ = writeln!(
out,
" let filter_fn: FilterFn = Arc::new(|db: ADb, pages: std::collections::HashMap<String, u64>| {{"
);
let _ = writeln!(out, " Box::pin(async move {{");
let _ = writeln!(
out,
" use sea_orm::{{ConnectionTrait, ExprTrait}};"
);
let _ = writeln!(
out,
" use sea_orm::sea_query::{{Query, Alias, Expr}};"
);
let _ = writeln!(
out,
" let mut result: std::collections::HashMap<String, (Vec<String>, u64)> = std::collections::HashMap::new();"
);
for (col, _label, limit) in &r.list_filter {
let _ = writeln!(
out,
" let page_size_{col} = {limit}u64;",
col = col,
limit = limit
);
let _ = writeln!(
out,
" let cur_page_{col} = pages.get(\"{col}\").copied().unwrap_or(0);",
col = col
);
let _ = writeln!(
out,
" let count_stmt_{col} = Query::select().expr(Expr::cust(\"COUNT(DISTINCT {col})\")).from(Alias::new({module}::Entity.table_name())).and_where(Expr::col(Alias::new(\"{col}\")).is_not_null()).to_owned();",
col = col,
module = module
);
let _ = writeln!(
out,
" let count_row_{col} = match db.query_one(&count_stmt_{col}).await {{ Ok(r) => r, Err(e) => {{ tracing::error!(\"[runique admin] list_filter `{key}.{col}`: column not found in DB — {{}}\", e); None }} }};",
col = col,
key = key
);
let _ = writeln!(
out,
" let total_{col} = count_row_{col}.and_then(|r| r.try_get_by_index::<i64>(0).ok()).unwrap_or(0) as u64;",
col = col
);
let _ = writeln!(
out,
" let stmt_{col} = Query::select().distinct().expr(Expr::cust(format!(\"CAST({col} AS {{}})\", text_cast_type(&db)))).from(Alias::new({module}::Entity.table_name())).and_where(Expr::col(Alias::new(\"{col}\")).is_not_null()).limit(page_size_{col}).offset(cur_page_{col} * page_size_{col}).to_owned();",
col = col,
module = module
);
let _ = writeln!(
out,
" let rows_{col} = match db.query_all(&stmt_{col}).await {{ Ok(r) => r, Err(e) => {{ tracing::error!(\"[runique admin] list_filter `{key}.{col}`: column not found in DB — {{}}\", e); vec![] }} }};",
col = col,
key = key
);
let _ = writeln!(
out,
" let mut vals_{col}: Vec<String> = rows_{col}.iter().filter_map(|r| r.try_get_by_index::<String>(0).ok()).collect(); vals_{col}.sort_by(|a, b| match (a.parse::<i64>(), b.parse::<i64>()) {{ (Ok(x), Ok(y)) => y.cmp(&x), _ => b.cmp(a) }});",
col = col
);
let _ = writeln!(
out,
" result.insert(\"{col}\".to_string(), (vals_{col}, total_{col}));",
col = col
);
}
let _ = writeln!(out, " Ok(result)");
let _ = writeln!(out, " }})");
let _ = writeln!(out, " }});");
let _ = writeln!(out);
}
let _ = writeln!(out, " registry.register(");
if r.edit_form_type.is_some() || r.bulk_create.is_some() {
let _ = writeln!(out, " ResourceEntry::new(meta, form_builder)");
let _ = writeln!(
out,
" .with_edit_form_builder(edit_form_builder)"
);
} else {
let _ = writeln!(out, " ResourceEntry::new(meta, form_builder)");
}
let _ = writeln!(out, " .with_list_fn(list_fn)");
let _ = writeln!(out, " .with_get_fn(get_fn)");
let _ = writeln!(
out,
" .with_enum_label_fn({}::apply_enum_labels)",
module
);
let _ = writeln!(out, " .with_delete_fn(delete_fn)");
let _ = writeln!(out, " .with_create_fn(create_fn)");
let _ = writeln!(out, " .with_update_fn(update_fn)");
let _ = writeln!(
out,
" .with_partial_update_fn(partial_update_fn)"
);
let _ = writeln!(out, " .with_count_fn(count_fn)");
let _ = writeln!(
out,
" .with_unique_fields({}::UNIQUE_FIELDS)",
module
);
if !r.list_filter.is_empty() {
let _ = writeln!(out, " .with_filter_fn(filter_fn)");
}
if !r.group_action.is_empty() {
let actions_str = build_group_actions_str(&r.group_action);
let _ = writeln!(
out,
" .with_group_actions(vec![{}])",
actions_str
);
}
if !r.m2m.is_empty() {
let _ = writeln!(out, " .with_m2m_loader(m2m_loader)");
}
if let Some(ref own_field) = r.own_field {
let _ = writeln!(out, " .with_own_field(\"{}\")", own_field);
}
let _ = writeln!(out, " );");
let _ = writeln!(out);
Ok(())
}
fn write_dyn_form_wrapper(
out: &mut String,
comment: &str,
wrapper: &str,
form_path: &str,
save_param: &str,
save_body: &str,
) -> Result<(), String> {
let _ = writeln!(out, "// ── {} ──", comment);
let _ = writeln!(out, "struct {}(pub {});", wrapper, form_path);
let _ = writeln!(out);
let _ = writeln!(out, "#[async_trait]");
let _ = writeln!(out, "impl DynForm for {} {{", wrapper);
let _ = writeln!(out, " async fn is_valid(&mut self) -> bool {{");
let _ = writeln!(out, " self.0.is_valid().await");
let _ = writeln!(out, " }}");
let _ = writeln!(out);
let _ = writeln!(
out,
" async fn save(&mut self, {}: &DatabaseConnection) -> Result<(), DbErr> {{",
save_param
);
let _ = writeln!(out, " {}", save_body);
let _ = writeln!(out, " }}");
let _ = writeln!(out);
let _ = writeln!(out, " fn get_form(&self) -> &Forms {{");
let _ = writeln!(out, " self.0.get_form()");
let _ = writeln!(out, " }}");
let _ = writeln!(out);
let _ = writeln!(out, " fn get_form_mut(&mut self) -> &mut Forms {{");
let _ = writeln!(out, " self.0.get_form_mut()");
let _ = writeln!(out, " }}");
let _ = writeln!(out, "}}");
let _ = writeln!(out);
Ok(())
}
fn write_form_builder_closure(out: &mut String, var_name: &str, form_path: &str, wrapper: &str) {
let _ = writeln!(
out,
" let {}: FormBuilder = Arc::new(|_db: ADb, _vec: Vec<std::string::String>, data: StrMap, tera: ATera, csrf: String, method: Method| {{",
var_name
);
let _ = writeln!(out, " Box::pin(async move {{");
let _ = writeln!(
out,
" let form = {}::build_with_data(&data, tera, &csrf, method).await;",
form_path
);
let _ = writeln!(
out,
" Box::new({}(form)) as Box<dyn DynForm>",
wrapper
);
let _ = writeln!(out, " }})");
let _ = writeln!(out, " }});");
let _ = writeln!(out);
}
fn write_form_builder_closure_fk(
out: &mut String,
var_name: &str,
form_path: &str,
wrapper: &str,
fk_cols: &[&(
String,
String,
Option<crate::admin::daemon::parser::FkDisplay>,
)],
) {
let db_param = if fk_cols.is_empty() { "_db" } else { "db" };
let _ = writeln!(
out,
" let {var}: FormBuilder = Arc::new(|{db}: ADb, _vec: Vec<std::string::String>, data: StrMap, tera: ATera, csrf: String, method: Method| {{",
var = var_name,
db = db_param,
);
let _ = writeln!(out, " Box::pin(async move {{");
let mut_kw = if fk_cols.is_empty() { "let" } else { "let mut" };
let _ = writeln!(
out,
" {mut_kw} form = {form}::build_with_data(&data, tera, &csrf, method).await;",
mut_kw = mut_kw,
form = form_path,
);
for (col, label, fk_opt) in fk_cols {
let fk = fk_opt.as_ref().unwrap();
let safe_var = col.replace('.', "_");
let _ = writeln!(out, " {{");
let _ = writeln!(out, " use sea_orm::ConnectionTrait;");
let _ = writeln!(
out,
" let _fk_opt_stmt_{safe} = sea_orm::sea_query::Query::select().expr(sea_orm::sea_query::Expr::cust(format!(\"CAST(id AS {{}})\", runique::admin::helper::text_cast_type(&db)))).expr(sea_orm::sea_query::Expr::cust(\"{fk_col}\")).from(sea_orm::sea_query::Alias::new(\"{fk_table}\")).to_owned();",
safe = safe_var,
fk_col = fk.col,
fk_table = fk.table,
);
let _ = writeln!(
out,
" let _fk_opt_choices_{safe}: Vec<(String, String)> = db.query_all(&_fk_opt_stmt_{safe}).await.unwrap_or_default().iter().filter_map(|row| {{ let id = row.try_get_by_index::<String>(0).ok()?; let lbl = row.try_get_by_index::<String>(1).ok()?; Some((id, lbl)) }}).collect();",
safe = safe_var,
);
let _ = writeln!(
out,
" form.get_form_mut().field_choices(\"{col}\", \"{label}\", _fk_opt_choices_{safe});",
col = col,
label = label,
safe = safe_var,
);
let _ = writeln!(out, " }}");
}
let _ = writeln!(
out,
" Box::new({wrapper}(form)) as Box<dyn DynForm>",
wrapper = wrapper,
);
let _ = writeln!(out, " }})");
let _ = writeln!(out, " }});");
let _ = writeln!(out);
}
fn write_search_conditions(
out: &mut String,
list_display: &[(
String,
String,
Option<crate::admin::daemon::parser::FkDisplay>,
)],
module: &str,
) {
let searchable: Vec<&str> = list_display
.iter()
.filter(|(_, _, fk)| fk.is_none())
.map(|(col, _, _)| col.as_str())
.collect();
if searchable.is_empty() {
let _ = writeln!(
out,
" let search_cond = search_cond!(&db => {module}::Entity => all_columns icontains search_str);",
module = module
);
} else {
let cols = searchable
.iter()
.map(|col| format!("\"{}\" icontains search_str", col))
.collect::<Vec<_>>()
.join(", ");
let _ = writeln!(
out,
" let search_cond = search_cond!(&db => {module}::Entity => or({cols}));",
module = module,
cols = cols
);
}
}
fn build_display_chain(
list_display: &[(
String,
String,
Option<crate::admin::daemon::parser::FkDisplay>,
)],
list_exclude: &[String],
list_filter: &[(String, String, u64)],
) -> String {
let mut chain = "DisplayConfig::new()".to_string();
if !list_display.is_empty() {
let cols = list_display
.iter()
.map(|(c, l, _)| format!("(\"{}\", \"{}\")", c, l))
.collect::<Vec<_>>()
.join(", ");
chain.push_str(&format!(".columns_include(vec![{}])", cols));
}
if !list_exclude.is_empty() {
let cols = list_exclude
.iter()
.map(|c| format!("\"{}\"", c))
.collect::<Vec<_>>()
.join(", ");
chain.push_str(&format!(".columns_exclude(vec![{}])", cols));
}
if !list_filter.is_empty() {
let filters = list_filter
.iter()
.map(|(col, label, limit)| format!("(\"{}\", \"{}\", {}u64)", col, label, limit))
.collect::<Vec<_>>()
.join(", ");
chain.push_str(&format!(".list_filter(vec![{}])", filters));
}
chain
}
fn build_group_actions_str(group_action: &[(String, String, Option<String>)]) -> String {
group_action
.iter()
.map(|(f, l, v)| match v {
Some(val) => format!("GroupAction::val(\"{}\", \"{}\", \"{}\")", f, l, val),
None => format!("GroupAction::bool(\"{}\", \"{}\")", f, l),
})
.collect::<Vec<_>>()
.join(", ")
}
fn full_model_path(model_type: &str) -> String {
if model_type.starts_with("crate::") || model_type.starts_with("runique::") {
model_type.to_string()
} else {
format!("crate::entities::{}", model_type)
}
}
fn pascal_case(s: &str) -> String {
s.split('_')
.map(|word| {
let mut c = word.chars();
match c.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + c.as_str(),
}
})
.collect()
}
fn model_to_module(model_type: &str) -> String {
let segments: Vec<&str> = model_type.split("::").collect();
if segments.len() >= 2 {
segments[segments.len().saturating_sub(2)].to_string()
} else {
let base = segments.last().copied().unwrap_or(model_type);
let mut result = String::new();
for (i, c) in base.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('_');
}
result.extend(c.to_lowercase());
}
result
}
}