use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldDefinition {
pub name: String,
pub label: String,
pub field_type: FieldType,
pub widget: WidgetType,
pub required: bool,
pub readonly: bool,
pub primary_key: bool,
pub list_display: bool,
pub searchable: bool,
pub filterable: bool,
pub sortable: bool,
pub default: Option<String>,
pub help_text: Option<String>,
pub placeholder: Option<String>,
pub validators: Vec<ValidatorType>,
pub choices: Option<Vec<Choice>>,
pub foreign_key: Option<ForeignKeyRef>,
pub max_length: Option<usize>,
pub min_value: Option<f64>,
pub max_value: Option<f64>,
}
impl FieldDefinition {
pub fn new(name: impl Into<String>, field_type: FieldType) -> Self {
let name = name.into();
let label = name
.replace('_', " ")
.split_whitespace()
.map(|w| {
let mut chars = w.chars();
match chars.next() {
Some(c) => c.to_uppercase().chain(chars).collect(),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join(" ");
Self {
name,
label,
widget: field_type.default_widget(),
field_type,
required: false,
readonly: false,
primary_key: false,
list_display: true,
searchable: false,
filterable: false,
sortable: true,
default: None,
help_text: None,
placeholder: None,
validators: Vec::new(),
choices: None,
foreign_key: None,
max_length: None,
min_value: None,
max_value: None,
}
}
pub fn required(mut self) -> Self {
self.required = true;
self
}
pub fn readonly(mut self) -> Self {
self.readonly = true;
self
}
pub fn primary_key(mut self) -> Self {
self.primary_key = true;
self.readonly = true;
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
pub fn widget(mut self, widget: WidgetType) -> Self {
self.widget = widget;
self
}
pub fn help_text(mut self, text: impl Into<String>) -> Self {
self.help_text = Some(text.into());
self
}
pub fn placeholder(mut self, text: impl Into<String>) -> Self {
self.placeholder = Some(text.into());
self
}
pub fn validator(mut self, validator: ValidatorType) -> Self {
self.validators.push(validator);
self
}
pub fn choices(mut self, choices: Vec<Choice>) -> Self {
self.choices = Some(choices);
self.widget = WidgetType::Select;
self
}
pub fn foreign_key(mut self, model: impl Into<String>, display_field: impl Into<String>) -> Self {
self.foreign_key = Some(ForeignKeyRef {
model: model.into(),
display_field: display_field.into(),
});
self.widget = WidgetType::ForeignKey;
self
}
pub fn searchable(mut self) -> Self {
self.searchable = true;
self
}
pub fn filterable(mut self) -> Self {
self.filterable = true;
self
}
pub fn hide_from_list(mut self) -> Self {
self.list_display = false;
self
}
pub fn no_sort(mut self) -> Self {
self.sortable = false;
self
}
pub fn max_length(mut self, len: usize) -> Self {
self.max_length = Some(len);
self
}
pub fn range(mut self, min: f64, max: f64) -> Self {
self.min_value = Some(min);
self.max_value = Some(max);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FieldType {
Integer,
BigInteger,
Float,
Decimal,
String,
Text,
Boolean,
Date,
Time,
DateTime,
Uuid,
Json,
Binary,
Email,
Url,
IpAddress,
Enum,
ForeignKey,
ManyToMany,
}
impl FieldType {
pub fn default_widget(&self) -> WidgetType {
match self {
Self::Integer | Self::BigInteger => WidgetType::NumberInput,
Self::Float | Self::Decimal => WidgetType::NumberInput,
Self::String => WidgetType::TextInput,
Self::Text => WidgetType::Textarea,
Self::Boolean => WidgetType::Checkbox,
Self::Date => WidgetType::DatePicker,
Self::Time => WidgetType::TimePicker,
Self::DateTime => WidgetType::DateTimePicker,
Self::Uuid => WidgetType::TextInput,
Self::Json => WidgetType::JsonEditor,
Self::Binary => WidgetType::FileUpload,
Self::Email => WidgetType::EmailInput,
Self::Url => WidgetType::UrlInput,
Self::IpAddress => WidgetType::TextInput,
Self::Enum => WidgetType::Select,
Self::ForeignKey => WidgetType::ForeignKey,
Self::ManyToMany => WidgetType::MultiSelect,
}
}
pub fn sql_type(&self) -> &'static str {
match self {
Self::Integer => "INTEGER",
Self::BigInteger => "BIGINT",
Self::Float => "REAL",
Self::Decimal => "DECIMAL",
Self::String => "VARCHAR",
Self::Text => "TEXT",
Self::Boolean => "BOOLEAN",
Self::Date => "DATE",
Self::Time => "TIME",
Self::DateTime => "TIMESTAMP",
Self::Uuid => "UUID",
Self::Json => "JSON",
Self::Binary => "BLOB",
Self::Email => "VARCHAR",
Self::Url => "VARCHAR",
Self::IpAddress => "VARCHAR",
Self::Enum => "VARCHAR",
Self::ForeignKey => "INTEGER",
Self::ManyToMany => "INTEGER",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WidgetType {
TextInput,
PasswordInput,
EmailInput,
UrlInput,
NumberInput,
Textarea,
RichText,
Checkbox,
Radio,
Select,
MultiSelect,
DatePicker,
TimePicker,
DateTimePicker,
ColorPicker,
FileUpload,
ImageUpload,
ForeignKey,
JsonEditor,
CodeEditor,
Hidden,
ReadOnly,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Choice {
pub value: String,
pub label: String,
pub disabled: bool,
}
impl Choice {
pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
Self {
value: value.into(),
label: label.into(),
disabled: false,
}
}
pub fn from_value(value: impl Into<String>) -> Self {
let value = value.into();
Self {
label: value.clone(),
value,
disabled: false,
}
}
pub fn disabled(mut self) -> Self {
self.disabled = true;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForeignKeyRef {
pub model: String,
pub display_field: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ValidatorType {
Required,
Email,
Url,
MinLength(usize),
MaxLength(usize),
MinValue(f64),
MaxValue(f64),
Pattern(String),
Custom(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_field_definition() {
let field = FieldDefinition::new("user_name", FieldType::String)
.required()
.searchable()
.max_length(100);
assert_eq!(field.name, "user_name");
assert_eq!(field.label, "User Name");
assert!(field.required);
assert!(field.searchable);
assert_eq!(field.max_length, Some(100));
}
#[test]
fn test_choice() {
let choice = Choice::new("active", "Active");
assert_eq!(choice.value, "active");
assert_eq!(choice.label, "Active");
assert!(!choice.disabled);
}
#[test]
fn test_field_type_widget() {
assert_eq!(FieldType::String.default_widget(), WidgetType::TextInput);
assert_eq!(FieldType::Boolean.default_widget(), WidgetType::Checkbox);
assert_eq!(FieldType::DateTime.default_widget(), WidgetType::DateTimePicker);
}
}