Skip to main content

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}