armature_admin/
field.rs

1//! Field definitions for admin models
2
3use serde::{Deserialize, Serialize};
4
5/// Field definition for a model
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct FieldDefinition {
8    /// Field name
9    pub name: String,
10    /// Display label
11    pub label: String,
12    /// Field type
13    pub field_type: FieldType,
14    /// Widget type for rendering
15    pub widget: WidgetType,
16    /// Is this field required?
17    pub required: bool,
18    /// Is this field read-only?
19    pub readonly: bool,
20    /// Is this the primary key?
21    pub primary_key: bool,
22    /// Show in list view?
23    pub list_display: bool,
24    /// Searchable?
25    pub searchable: bool,
26    /// Filterable?
27    pub filterable: bool,
28    /// Sortable?
29    pub sortable: bool,
30    /// Default value
31    pub default: Option<String>,
32    /// Help text
33    pub help_text: Option<String>,
34    /// Placeholder text
35    pub placeholder: Option<String>,
36    /// Validation rules
37    pub validators: Vec<ValidatorType>,
38    /// Choices for select fields
39    pub choices: Option<Vec<Choice>>,
40    /// Foreign key reference
41    pub foreign_key: Option<ForeignKeyRef>,
42    /// Maximum length (for strings)
43    pub max_length: Option<usize>,
44    /// Minimum value (for numbers)
45    pub min_value: Option<f64>,
46    /// Maximum value (for numbers)
47    pub max_value: Option<f64>,
48}
49
50impl FieldDefinition {
51    /// Create a new field definition
52    pub fn new(name: impl Into<String>, field_type: FieldType) -> Self {
53        let name = name.into();
54        let label = name
55            .replace('_', " ")
56            .split_whitespace()
57            .map(|w| {
58                let mut chars = w.chars();
59                match chars.next() {
60                    Some(c) => c.to_uppercase().chain(chars).collect(),
61                    None => String::new(),
62                }
63            })
64            .collect::<Vec<_>>()
65            .join(" ");
66
67        Self {
68            name,
69            label,
70            widget: field_type.default_widget(),
71            field_type,
72            required: false,
73            readonly: false,
74            primary_key: false,
75            list_display: true,
76            searchable: false,
77            filterable: false,
78            sortable: true,
79            default: None,
80            help_text: None,
81            placeholder: None,
82            validators: Vec::new(),
83            choices: None,
84            foreign_key: None,
85            max_length: None,
86            min_value: None,
87            max_value: None,
88        }
89    }
90
91    /// Set field as required
92    pub fn required(mut self) -> Self {
93        self.required = true;
94        self
95    }
96
97    /// Set field as read-only
98    pub fn readonly(mut self) -> Self {
99        self.readonly = true;
100        self
101    }
102
103    /// Set as primary key
104    pub fn primary_key(mut self) -> Self {
105        self.primary_key = true;
106        self.readonly = true;
107        self
108    }
109
110    /// Set custom label
111    pub fn label(mut self, label: impl Into<String>) -> Self {
112        self.label = label.into();
113        self
114    }
115
116    /// Set widget type
117    pub fn widget(mut self, widget: WidgetType) -> Self {
118        self.widget = widget;
119        self
120    }
121
122    /// Set help text
123    pub fn help_text(mut self, text: impl Into<String>) -> Self {
124        self.help_text = Some(text.into());
125        self
126    }
127
128    /// Set placeholder
129    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
130        self.placeholder = Some(text.into());
131        self
132    }
133
134    /// Add validator
135    pub fn validator(mut self, validator: ValidatorType) -> Self {
136        self.validators.push(validator);
137        self
138    }
139
140    /// Set choices
141    pub fn choices(mut self, choices: Vec<Choice>) -> Self {
142        self.choices = Some(choices);
143        self.widget = WidgetType::Select;
144        self
145    }
146
147    /// Set foreign key
148    pub fn foreign_key(mut self, model: impl Into<String>, display_field: impl Into<String>) -> Self {
149        self.foreign_key = Some(ForeignKeyRef {
150            model: model.into(),
151            display_field: display_field.into(),
152        });
153        self.widget = WidgetType::ForeignKey;
154        self
155    }
156
157    /// Enable search
158    pub fn searchable(mut self) -> Self {
159        self.searchable = true;
160        self
161    }
162
163    /// Enable filter
164    pub fn filterable(mut self) -> Self {
165        self.filterable = true;
166        self
167    }
168
169    /// Hide from list
170    pub fn hide_from_list(mut self) -> Self {
171        self.list_display = false;
172        self
173    }
174
175    /// Disable sorting
176    pub fn no_sort(mut self) -> Self {
177        self.sortable = false;
178        self
179    }
180
181    /// Set max length
182    pub fn max_length(mut self, len: usize) -> Self {
183        self.max_length = Some(len);
184        self
185    }
186
187    /// Set value range
188    pub fn range(mut self, min: f64, max: f64) -> Self {
189        self.min_value = Some(min);
190        self.max_value = Some(max);
191        self
192    }
193}
194
195/// Field types
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
197pub enum FieldType {
198    /// Integer
199    Integer,
200    /// Big integer
201    BigInteger,
202    /// Float
203    Float,
204    /// Decimal
205    Decimal,
206    /// String
207    String,
208    /// Text (long string)
209    Text,
210    /// Boolean
211    Boolean,
212    /// Date
213    Date,
214    /// Time
215    Time,
216    /// DateTime
217    DateTime,
218    /// UUID
219    Uuid,
220    /// JSON
221    Json,
222    /// Binary/Blob
223    Binary,
224    /// Email
225    Email,
226    /// URL
227    Url,
228    /// IP Address
229    IpAddress,
230    /// Enum/Choices
231    Enum,
232    /// Foreign key
233    ForeignKey,
234    /// Many-to-many
235    ManyToMany,
236}
237
238impl FieldType {
239    /// Get default widget for this field type
240    pub fn default_widget(&self) -> WidgetType {
241        match self {
242            Self::Integer | Self::BigInteger => WidgetType::NumberInput,
243            Self::Float | Self::Decimal => WidgetType::NumberInput,
244            Self::String => WidgetType::TextInput,
245            Self::Text => WidgetType::Textarea,
246            Self::Boolean => WidgetType::Checkbox,
247            Self::Date => WidgetType::DatePicker,
248            Self::Time => WidgetType::TimePicker,
249            Self::DateTime => WidgetType::DateTimePicker,
250            Self::Uuid => WidgetType::TextInput,
251            Self::Json => WidgetType::JsonEditor,
252            Self::Binary => WidgetType::FileUpload,
253            Self::Email => WidgetType::EmailInput,
254            Self::Url => WidgetType::UrlInput,
255            Self::IpAddress => WidgetType::TextInput,
256            Self::Enum => WidgetType::Select,
257            Self::ForeignKey => WidgetType::ForeignKey,
258            Self::ManyToMany => WidgetType::MultiSelect,
259        }
260    }
261
262    /// Get SQL type representation
263    pub fn sql_type(&self) -> &'static str {
264        match self {
265            Self::Integer => "INTEGER",
266            Self::BigInteger => "BIGINT",
267            Self::Float => "REAL",
268            Self::Decimal => "DECIMAL",
269            Self::String => "VARCHAR",
270            Self::Text => "TEXT",
271            Self::Boolean => "BOOLEAN",
272            Self::Date => "DATE",
273            Self::Time => "TIME",
274            Self::DateTime => "TIMESTAMP",
275            Self::Uuid => "UUID",
276            Self::Json => "JSON",
277            Self::Binary => "BLOB",
278            Self::Email => "VARCHAR",
279            Self::Url => "VARCHAR",
280            Self::IpAddress => "VARCHAR",
281            Self::Enum => "VARCHAR",
282            Self::ForeignKey => "INTEGER",
283            Self::ManyToMany => "INTEGER",
284        }
285    }
286}
287
288/// Widget types for form rendering
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
290pub enum WidgetType {
291    /// Text input
292    TextInput,
293    /// Password input
294    PasswordInput,
295    /// Email input
296    EmailInput,
297    /// URL input
298    UrlInput,
299    /// Number input
300    NumberInput,
301    /// Textarea
302    Textarea,
303    /// Rich text editor
304    RichText,
305    /// Checkbox
306    Checkbox,
307    /// Radio buttons
308    Radio,
309    /// Select dropdown
310    Select,
311    /// Multi-select
312    MultiSelect,
313    /// Date picker
314    DatePicker,
315    /// Time picker
316    TimePicker,
317    /// DateTime picker
318    DateTimePicker,
319    /// Color picker
320    ColorPicker,
321    /// File upload
322    FileUpload,
323    /// Image upload
324    ImageUpload,
325    /// Foreign key selector
326    ForeignKey,
327    /// JSON editor
328    JsonEditor,
329    /// Code editor
330    CodeEditor,
331    /// Hidden field
332    Hidden,
333    /// Read-only display
334    ReadOnly,
335}
336
337/// Choice for select fields
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct Choice {
340    /// Value stored
341    pub value: String,
342    /// Display label
343    pub label: String,
344    /// Is disabled?
345    pub disabled: bool,
346}
347
348impl Choice {
349    /// Create a new choice
350    pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
351        Self {
352            value: value.into(),
353            label: label.into(),
354            disabled: false,
355        }
356    }
357
358    /// Create from value (label = value)
359    pub fn from_value(value: impl Into<String>) -> Self {
360        let value = value.into();
361        Self {
362            label: value.clone(),
363            value,
364            disabled: false,
365        }
366    }
367
368    /// Set as disabled
369    pub fn disabled(mut self) -> Self {
370        self.disabled = true;
371        self
372    }
373}
374
375/// Foreign key reference
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct ForeignKeyRef {
378    /// Related model name
379    pub model: String,
380    /// Field to display
381    pub display_field: String,
382}
383
384/// Validator types
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub enum ValidatorType {
387    /// Required field
388    Required,
389    /// Email format
390    Email,
391    /// URL format
392    Url,
393    /// Minimum length
394    MinLength(usize),
395    /// Maximum length
396    MaxLength(usize),
397    /// Minimum value
398    MinValue(f64),
399    /// Maximum value
400    MaxValue(f64),
401    /// Regex pattern
402    Pattern(String),
403    /// Custom validator name
404    Custom(String),
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    #[test]
412    fn test_field_definition() {
413        let field = FieldDefinition::new("user_name", FieldType::String)
414            .required()
415            .searchable()
416            .max_length(100);
417
418        assert_eq!(field.name, "user_name");
419        assert_eq!(field.label, "User Name");
420        assert!(field.required);
421        assert!(field.searchable);
422        assert_eq!(field.max_length, Some(100));
423    }
424
425    #[test]
426    fn test_choice() {
427        let choice = Choice::new("active", "Active");
428        assert_eq!(choice.value, "active");
429        assert_eq!(choice.label, "Active");
430        assert!(!choice.disabled);
431    }
432
433    #[test]
434    fn test_field_type_widget() {
435        assert_eq!(FieldType::String.default_widget(), WidgetType::TextInput);
436        assert_eq!(FieldType::Boolean.default_widget(), WidgetType::Checkbox);
437        assert_eq!(FieldType::DateTime.default_widget(), WidgetType::DateTimePicker);
438    }
439}
440