Skip to main content

rustio_core/admin/
auto_form.rs

1//! Auto form generation — derive a [`FormConfig`] from a Rust type
2//! that implements [`FormModel`], no manual field-by-field wiring.
3//!
4//! Callers describe their model once (as a `Vec<AutoField>` in
5//! `form_fields`), and [`form_from_model`] or the fluent
6//! [`FormBuilder`] produce the same [`FormConfig`] the manual
7//! engine consumes. The existing [`crate::admin::form`] renderer is
8//! untouched — this module is purely additive.
9//!
10//! No macros, no runtime reflection, no DB introspection. Pure typed
11//! metadata supplied by the caller.
12
13use crate::admin::form::{infer_field_type, FieldConfig, FieldType, FormConfig};
14
15// ---------------------------------------------------------------
16// Model-side description
17// ---------------------------------------------------------------
18
19/// Declarative description of one field on a model. The caller fills
20/// this in once per column; the converter turns it into a
21/// [`FieldConfig`].
22///
23/// `field_type = None` asks the engine to infer the type from the
24/// field's `name` (via [`infer_field_type`]). `is_foreign_key = true`
25/// always wins — even if `field_type` or inference says otherwise,
26/// the final widget is a `<select>` populated from `options`.
27#[derive(Debug, Clone)]
28pub struct AutoField {
29    pub name: &'static str,
30    pub label: &'static str,
31
32    /// If `None`, [`infer_field_type`] runs against `name`.
33    pub field_type: Option<FieldType>,
34
35    pub required: bool,
36
37    /// Forces [`FieldType::ForeignKey`], overriding both `field_type`
38    /// and inference. The FK is always rendered as a `<select>` with
39    /// `options` supplying human-readable labels.
40    pub is_foreign_key: bool,
41
42    /// `(value, label)` pairs consumed by Select / ForeignKey.
43    pub options: Vec<(String, String)>,
44}
45
46/// Types that can describe their own form shape.
47///
48/// Invoked through the turbofish: `FormBuilder::from_model::<User>()`
49/// — no instance needed, so this composes with unit structs used
50/// purely as type tags.
51pub trait FormModel {
52    fn form_fields() -> Vec<AutoField>;
53    fn form_title() -> &'static str;
54}
55
56// ---------------------------------------------------------------
57// Conversion
58// ---------------------------------------------------------------
59
60/// Build a [`FormConfig`] from a model's [`FormModel`] impl. The
61/// resulting form has no `value`s and no `help` — overrides populate
62/// those via [`FormBuilder::override_field`].
63pub fn form_from_model<T: FormModel>() -> FormConfig {
64    let fields = T::form_fields()
65        .into_iter()
66        .map(field_config_from)
67        .collect();
68
69    FormConfig {
70        title: T::form_title().to_string(),
71        // Subtitle isn't part of the FormModel contract today; a
72        // future extension could derive it from a type name or an
73        // extra trait method. Empty string renders as an empty
74        // `.drawer-subtitle` div, which the approved CSS handles.
75        subtitle: String::new(),
76        fields,
77        submitted: false,
78        save_failed: false,
79        hidden_fields: Vec::new(),
80    }
81}
82
83fn field_config_from(f: AutoField) -> FieldConfig {
84    // 1. Start from the caller's declared type, falling back to name
85    //    inference when the caller left it blank.
86    let mut ty = match f.field_type {
87        Some(t) => t,
88        None => infer_field_type(f.name),
89    };
90    // 2. `is_foreign_key` is the hard override — it always wins,
91    //    even against a caller-declared `field_type`. Matches the
92    //    invariant "FK columns render as dropdowns, never raw ids".
93    if f.is_foreign_key {
94        ty = FieldType::ForeignKey;
95    }
96    FieldConfig {
97        name: f.name.to_string(),
98        label: f.label.to_string(),
99        field_type: ty,
100        required: f.required,
101        readonly: false,
102        placeholder: None,
103        help: None,
104        value: None,
105        options: f.options,
106        error: None,
107    }
108}
109
110// ---------------------------------------------------------------
111// Override / builder
112// ---------------------------------------------------------------
113
114/// Patch applied to a single field after auto-generation. Every
115/// sub-field is `Option` — `None` means "leave as the generated
116/// value".
117#[derive(Debug, Clone, Default)]
118pub struct FieldOverride {
119    pub field_type: Option<FieldType>,
120    pub label: Option<String>,
121    pub help: Option<String>,
122}
123
124/// Fluent builder that starts from an auto-generated [`FormConfig`]
125/// and lets the caller patch individual fields. An
126/// `override_field(name, ...)` call whose `name` doesn't match any
127/// generated field is a silent no-op — the builder intentionally
128/// doesn't fail on typos so it composes with optional overrides from
129/// config files.
130pub struct FormBuilder {
131    pub form: FormConfig,
132}
133
134impl FormBuilder {
135    pub fn from_model<T: FormModel>() -> Self {
136        Self {
137            form: form_from_model::<T>(),
138        }
139    }
140
141    pub fn override_field(mut self, name: &str, patch: FieldOverride) -> Self {
142        if let Some(field) = self.form.fields.iter_mut().find(|f| f.name == name) {
143            if let Some(ty) = patch.field_type {
144                field.field_type = ty;
145            }
146            if let Some(label) = patch.label {
147                field.label = label;
148            }
149            if let Some(help) = patch.help {
150                field.help = Some(help);
151            }
152        }
153        self
154    }
155
156    pub fn build(self) -> FormConfig {
157        self.form
158    }
159}