Skip to main content

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(
149        mut self,
150        model: impl Into<String>,
151        display_field: impl Into<String>,
152    ) -> Self {
153        self.foreign_key = Some(ForeignKeyRef {
154            model: model.into(),
155            display_field: display_field.into(),
156        });
157        self.widget = WidgetType::ForeignKey;
158        self
159    }
160
161    /// Enable search
162    pub fn searchable(mut self) -> Self {
163        self.searchable = true;
164        self
165    }
166
167    /// Enable filter
168    pub fn filterable(mut self) -> Self {
169        self.filterable = true;
170        self
171    }
172
173    /// Hide from list
174    pub fn hide_from_list(mut self) -> Self {
175        self.list_display = false;
176        self
177    }
178
179    /// Disable sorting
180    pub fn no_sort(mut self) -> Self {
181        self.sortable = false;
182        self
183    }
184
185    /// Set max length
186    pub fn max_length(mut self, len: usize) -> Self {
187        self.max_length = Some(len);
188        self
189    }
190
191    /// Set value range
192    pub fn range(mut self, min: f64, max: f64) -> Self {
193        self.min_value = Some(min);
194        self.max_value = Some(max);
195        self
196    }
197}
198
199/// Field types
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
201pub enum FieldType {
202    /// Integer
203    Integer,
204    /// Big integer
205    BigInteger,
206    /// Float
207    Float,
208    /// Decimal
209    Decimal,
210    /// String
211    String,
212    /// Text (long string)
213    Text,
214    /// Boolean
215    Boolean,
216    /// Date
217    Date,
218    /// Time
219    Time,
220    /// DateTime
221    DateTime,
222    /// UUID
223    Uuid,
224    /// JSON
225    Json,
226    /// Binary/Blob
227    Binary,
228    /// Email
229    Email,
230    /// URL
231    Url,
232    /// IP Address
233    IpAddress,
234    /// Enum/Choices
235    Enum,
236    /// Foreign key
237    ForeignKey,
238    /// Many-to-many
239    ManyToMany,
240}
241
242impl FieldType {
243    /// Get default widget for this field type
244    pub fn default_widget(&self) -> WidgetType {
245        match self {
246            Self::Integer | Self::BigInteger => WidgetType::NumberInput,
247            Self::Float | Self::Decimal => WidgetType::NumberInput,
248            Self::String => WidgetType::TextInput,
249            Self::Text => WidgetType::Textarea,
250            Self::Boolean => WidgetType::Checkbox,
251            Self::Date => WidgetType::DatePicker,
252            Self::Time => WidgetType::TimePicker,
253            Self::DateTime => WidgetType::DateTimePicker,
254            Self::Uuid => WidgetType::TextInput,
255            Self::Json => WidgetType::JsonEditor,
256            Self::Binary => WidgetType::FileUpload,
257            Self::Email => WidgetType::EmailInput,
258            Self::Url => WidgetType::UrlInput,
259            Self::IpAddress => WidgetType::TextInput,
260            Self::Enum => WidgetType::Select,
261            Self::ForeignKey => WidgetType::ForeignKey,
262            Self::ManyToMany => WidgetType::MultiSelect,
263        }
264    }
265}
266
267/// Widget types for form rendering
268#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
269pub enum WidgetType {
270    /// Text input
271    TextInput,
272    /// Password input
273    PasswordInput,
274    /// Email input
275    EmailInput,
276    /// URL input
277    UrlInput,
278    /// Number input
279    NumberInput,
280    /// Textarea
281    Textarea,
282    /// Rich text editor
283    RichText,
284    /// Checkbox
285    Checkbox,
286    /// Radio buttons
287    Radio,
288    /// Select dropdown
289    Select,
290    /// Multi-select
291    MultiSelect,
292    /// Date picker
293    DatePicker,
294    /// Time picker
295    TimePicker,
296    /// DateTime picker
297    DateTimePicker,
298    /// Color picker
299    ColorPicker,
300    /// File upload
301    FileUpload,
302    /// Image upload
303    ImageUpload,
304    /// Foreign key selector
305    ForeignKey,
306    /// JSON editor
307    JsonEditor,
308    /// Code editor
309    CodeEditor,
310    /// Hidden field
311    Hidden,
312    /// Read-only display
313    ReadOnly,
314}
315
316/// Choice for select fields
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct Choice {
319    /// Value stored
320    pub value: String,
321    /// Display label
322    pub label: String,
323    /// Is disabled?
324    pub disabled: bool,
325}
326
327impl Choice {
328    /// Create a new choice
329    pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
330        Self {
331            value: value.into(),
332            label: label.into(),
333            disabled: false,
334        }
335    }
336
337    /// Create from value (label = value)
338    pub fn from_value(value: impl Into<String>) -> Self {
339        let value = value.into();
340        Self {
341            label: value.clone(),
342            value,
343            disabled: false,
344        }
345    }
346
347    /// Set as disabled
348    pub fn disabled(mut self) -> Self {
349        self.disabled = true;
350        self
351    }
352}
353
354/// Foreign key reference
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct ForeignKeyRef {
357    /// Related model name
358    pub model: String,
359    /// Field to display
360    pub display_field: String,
361}
362
363/// Validator types
364#[derive(Debug, Clone, Serialize, Deserialize)]
365pub enum ValidatorType {
366    /// Required field
367    Required,
368    /// Email format
369    Email,
370    /// URL format
371    Url,
372    /// Minimum length
373    MinLength(usize),
374    /// Maximum length
375    MaxLength(usize),
376    /// Minimum value
377    MinValue(f64),
378    /// Maximum value
379    MaxValue(f64),
380    /// Regex pattern
381    Pattern(String),
382    /// Custom validator name
383    Custom(String),
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn test_field_definition() {
392        let field = FieldDefinition::new("user_name", FieldType::String)
393            .required()
394            .searchable()
395            .max_length(100);
396
397        assert_eq!(field.name, "user_name");
398        assert_eq!(field.label, "User Name");
399        assert!(field.required);
400        assert!(field.searchable);
401        assert_eq!(field.max_length, Some(100));
402    }
403
404    #[test]
405    fn test_choice() {
406        let choice = Choice::new("active", "Active");
407        assert_eq!(choice.value, "active");
408        assert_eq!(choice.label, "Active");
409        assert!(!choice.disabled);
410    }
411
412    #[test]
413    fn test_field_type_widget() {
414        assert_eq!(FieldType::String.default_widget(), WidgetType::TextInput);
415        assert_eq!(FieldType::Boolean.default_widget(), WidgetType::Checkbox);
416        assert_eq!(
417            FieldType::DateTime.default_widget(),
418            WidgetType::DateTimePicker
419        );
420    }
421}