rustio_core/admin/admin_generator.rs
1//! Config-driven admin model generator.
2//!
3//! [`AdminModelConfig`] holds the same metadata a hand-written
4//! [`AdminUiModel`] impl exposes (slug, table, fields, search /
5//! status / ensure-table SQL). [`GeneratedAdminModel`] then wraps
6//! one of these configs and implements [`AdminUiModel`] by simply
7//! delegating to the stored config — no per-model code path.
8//!
9//! The result is that registering a new admin section becomes a
10//! single declarative call:
11//!
12//! ```ignore
13//! register_generated(
14//! &mut registry,
15//! AdminModelConfig::new("orders", "Order")
16//! .table("admin_new_demo_orders")
17//! .primary_key("id")
18//! .fields(vec![
19//! AdminUiField::text("order_number", "Order #")
20//! .required(true).filterable(true).sortable(true),
21//! …
22//! ])
23//! .searchable(vec!["order_number", "customer_email"])
24//! .status_field("is_paid")
25//! .ensure_sql("CREATE TABLE IF NOT EXISTS …"),
26//! );
27//! ```
28//!
29//! All routes (`/admin-new/<slug>`, search, filters, sort, pagination,
30//! bulk actions, row delete, edit drawer) keep working unchanged —
31//! they consume `&dyn AdminUiModel`, and a `GeneratedAdminModel`
32//! satisfies that contract just like any unit-struct impl.
33
34use crate::admin::admin_form_bridge::{AdminUiField, AdminUiModel};
35
36/// Declarative description of an admin section. Cheap to clone —
37/// the registry's factory closure clones one of these on every
38/// lookup so each request gets a fresh boxed model.
39#[derive(Debug, Clone)]
40pub struct AdminModelConfig {
41 pub slug: &'static str,
42 pub model_name: &'static str,
43 pub table_name: &'static str,
44 pub primary_key: &'static str,
45 pub fields: Vec<AdminUiField>,
46 pub searchable_fields: Vec<&'static str>,
47 pub primary_status_field: Option<&'static str>,
48 /// `Some(sql)` runs an idempotent `CREATE TABLE IF NOT EXISTS …`
49 /// on every request; `None` skips auto-creation (caller owns
50 /// migrations). Mirrors the [`AdminUiModel::ensure_table_sql`]
51 /// contract directly.
52 pub ensure_table_sql: Option<&'static str>,
53}
54
55impl AdminModelConfig {
56 /// Start a new config with required identity fields. Defaults:
57 /// `table_name = ""`, `primary_key = "id"`, no fields, nothing
58 /// searchable, no status column, no ensure-table SQL. Call the
59 /// builder methods below before registration.
60 pub fn new(slug: &'static str, model_name: &'static str) -> Self {
61 Self {
62 slug,
63 model_name,
64 table_name: "",
65 primary_key: "id",
66 fields: Vec::new(),
67 searchable_fields: Vec::new(),
68 primary_status_field: None,
69 ensure_table_sql: None,
70 }
71 }
72
73 pub fn table(mut self, table_name: &'static str) -> Self {
74 self.table_name = table_name;
75 self
76 }
77
78 pub fn primary_key(mut self, primary_key: &'static str) -> Self {
79 self.primary_key = primary_key;
80 self
81 }
82
83 pub fn fields(mut self, fields: Vec<AdminUiField>) -> Self {
84 self.fields = fields;
85 self
86 }
87
88 pub fn searchable(mut self, searchable_fields: Vec<&'static str>) -> Self {
89 self.searchable_fields = searchable_fields;
90 self
91 }
92
93 pub fn status_field(mut self, name: &'static str) -> Self {
94 self.primary_status_field = Some(name);
95 self
96 }
97
98 pub fn ensure_sql(mut self, sql: &'static str) -> Self {
99 self.ensure_table_sql = Some(sql);
100 self
101 }
102}
103
104/// Adapter that turns an [`AdminModelConfig`] into an
105/// [`AdminUiModel`]. Holds the config by value so the registry's
106/// closure can construct one per request without lifetime gymnastics.
107pub struct GeneratedAdminModel {
108 pub config: AdminModelConfig,
109}
110
111impl AdminUiModel for GeneratedAdminModel {
112 fn slug(&self) -> &'static str {
113 self.config.slug
114 }
115 fn model_name(&self) -> &'static str {
116 self.config.model_name
117 }
118 fn table_name(&self) -> &'static str {
119 self.config.table_name
120 }
121 fn primary_key(&self) -> &'static str {
122 self.config.primary_key
123 }
124 fn fields(&self) -> Vec<AdminUiField> {
125 self.config.fields.clone()
126 }
127 fn searchable_fields(&self) -> Vec<&'static str> {
128 self.config.searchable_fields.clone()
129 }
130 fn primary_status_field(&self) -> Option<&'static str> {
131 self.config.primary_status_field
132 }
133 fn ensure_table_sql(&self) -> Option<&'static str> {
134 self.config.ensure_table_sql
135 }
136}
137
138/// Convenience factory matching the registry's `Fn() -> Box<dyn
139/// AdminUiModel>` shape.
140pub fn from_config(config: AdminModelConfig) -> Box<dyn AdminUiModel> {
141 Box::new(GeneratedAdminModel { config })
142}