use crate::{CharField, EmailField, FloatField, Form, FormError, FormField, IntegerField, Widget};
use serde_json::Value;
use std::collections::HashMap;
use std::marker::PhantomData;
#[derive(Debug, Clone)]
pub enum FieldType {
Char {
max_length: Option<usize>,
},
Text,
Integer,
Float,
Boolean,
DateTime,
Date,
Time,
Email,
Url,
Json,
}
pub trait FormModel: Send + Sync {
fn field_names() -> Vec<String>;
fn field_type(_name: &str) -> Option<FieldType> {
None
}
fn get_field(&self, name: &str) -> Option<Value>;
fn set_field(&mut self, name: &str, value: Value) -> Result<(), String>;
fn save(&mut self) -> Result<(), String>;
fn validate(&self) -> Result<(), Vec<String>> {
Ok(())
}
fn to_choice_label(&self) -> String {
self.get_field("id")
.and_then(|v| v.as_i64().map(|i| i.to_string()))
.or_else(|| {
self.get_field("id")
.and_then(|v| v.as_str().map(|s| s.to_string()))
})
.unwrap_or_default()
}
fn to_choice_value(&self) -> String {
self.get_field("id")
.and_then(|v| v.as_i64().map(|i| i.to_string()))
.or_else(|| {
self.get_field("id")
.and_then(|v| v.as_str().map(|s| s.to_string()))
})
.unwrap_or_default()
}
}
#[derive(Debug, Clone, Default)]
pub struct ModelFormConfig {
pub fields: Option<Vec<String>>,
pub exclude: Vec<String>,
pub widgets: HashMap<String, crate::Widget>,
pub labels: HashMap<String, String>,
pub help_texts: HashMap<String, String>,
}
impl ModelFormConfig {
pub fn new() -> Self {
Self::default()
}
pub fn fields(mut self, fields: Vec<String>) -> Self {
self.fields = Some(fields);
self
}
pub fn exclude(mut self, exclude: Vec<String>) -> Self {
self.exclude = exclude;
self
}
pub fn widget(mut self, field: String, widget: crate::Widget) -> Self {
self.widgets.insert(field, widget);
self
}
pub fn label(mut self, field: String, label: String) -> Self {
self.labels.insert(field, label);
self
}
pub fn help_text(mut self, field: String, text: String) -> Self {
self.help_texts.insert(field, text);
self
}
}
pub struct ModelForm<T: FormModel> {
form: Form,
instance: Option<T>,
#[allow(dead_code)]
config: ModelFormConfig,
_phantom: PhantomData<T>,
}
impl<T: FormModel> ModelForm<T> {
fn create_form_field(
name: &str,
field_type: FieldType,
config: &ModelFormConfig,
) -> Box<dyn FormField> {
let label = config.labels.get(name).cloned();
let help_text = config.help_texts.get(name).cloned();
let widget = config.widgets.get(name).cloned();
match field_type {
FieldType::Char { max_length } => {
let mut field = CharField::new(name.to_string());
if let Some(label) = label {
field.label = Some(label);
}
if let Some(help) = help_text {
field.help_text = Some(help);
}
if let Some(w) = widget {
field.widget = w;
}
field.max_length = max_length;
Box::new(field)
}
FieldType::Text => {
let mut field = CharField::new(name.to_string());
if let Some(label) = label {
field.label = Some(label);
}
if let Some(help) = help_text {
field.help_text = Some(help);
}
if let Some(w) = widget {
field.widget = w;
} else {
field.widget = Widget::TextArea;
}
Box::new(field)
}
FieldType::Email => {
let mut field = EmailField::new(name.to_string());
if let Some(label) = label {
field.label = Some(label);
}
if let Some(help) = help_text {
field.help_text = Some(help);
}
if let Some(w) = widget {
field.widget = w;
}
Box::new(field)
}
FieldType::Integer => {
let mut field = IntegerField::new(name.to_string());
if let Some(label) = label {
field.label = Some(label);
}
if let Some(help) = help_text {
field.help_text = Some(help);
}
if let Some(w) = widget {
field.widget = w;
}
Box::new(field)
}
FieldType::Float => {
let mut field = FloatField::new(name.to_string());
if let Some(label) = label {
field.label = Some(label);
}
if let Some(help) = help_text {
field.help_text = Some(help);
}
if let Some(w) = widget {
field.widget = w;
}
Box::new(field)
}
_ => {
let mut field = CharField::new(name.to_string());
if let Some(label) = label {
field.label = Some(label);
}
if let Some(help) = help_text {
field.help_text = Some(help);
}
if let Some(w) = widget {
field.widget = w;
}
Box::new(field)
}
}
}
pub fn new(instance: Option<T>, config: ModelFormConfig) -> Self {
let mut form = Form::new();
let all_fields = T::field_names();
let fields_to_include: Vec<String> = if let Some(ref include) = config.fields {
include
.iter()
.filter(|f| !config.exclude.contains(f))
.cloned()
.collect()
} else {
all_fields
.iter()
.filter(|f| !config.exclude.contains(f))
.cloned()
.collect()
};
for field_name in &fields_to_include {
if let Some(field_type) = T::field_type(field_name) {
let form_field = Self::create_form_field(field_name, field_type, &config);
form.add_field(form_field);
}
}
if let Some(ref inst) = instance {
let mut initial = HashMap::new();
for field_name in &fields_to_include {
if let Some(value) = inst.get_field(field_name) {
initial.insert(field_name.clone(), value);
}
}
form.bind(initial);
}
Self {
form,
instance,
config,
_phantom: PhantomData,
}
}
pub fn empty(config: ModelFormConfig) -> Self {
Self::new(None, config)
}
pub fn bind(&mut self, data: HashMap<String, Value>) -> &mut Self {
self.form.bind(data);
self
}
pub fn is_valid(&mut self) -> bool {
if let Some(ref instance) = self.instance
&& let Err(_errors) = instance.validate()
{
return false;
}
true
}
pub fn save(&mut self) -> Result<T, FormError> {
if !self.is_valid() {
return Err(FormError::Validation("Form is not valid".to_string()));
}
let mut instance = self.instance.take().ok_or(FormError::NoInstance)?;
let cleaned_data = self.form.cleaned_data();
for (field_name, value) in cleaned_data.iter() {
if let Err(e) = instance.set_field(field_name, value.clone()) {
return Err(FormError::Validation(format!(
"Failed to set field {}: {}",
field_name, e
)));
}
}
if let Err(e) = instance.save() {
return Err(FormError::Validation(format!("Failed to save: {}", e)));
}
Ok(instance)
}
pub fn set_field_value(&mut self, field_name: &str, value: Value) {
if let Some(ref mut instance) = self.instance {
let _ = instance.set_field(field_name, value);
}
}
pub fn form(&self) -> &Form {
&self.form
}
pub fn form_mut(&mut self) -> &mut Form {
&mut self.form
}
pub fn instance(&self) -> Option<&T> {
self.instance.as_ref()
}
}
pub struct ModelFormBuilder<T: FormModel> {
config: ModelFormConfig,
_phantom: PhantomData<T>,
}
impl<T: FormModel> ModelFormBuilder<T> {
pub fn new() -> Self {
Self {
config: ModelFormConfig::default(),
_phantom: PhantomData,
}
}
pub fn fields(mut self, fields: Vec<String>) -> Self {
self.config.fields = Some(fields);
self
}
pub fn exclude(mut self, exclude: Vec<String>) -> Self {
self.config.exclude = exclude;
self
}
pub fn widget(mut self, field: String, widget: crate::Widget) -> Self {
self.config.widgets.insert(field, widget);
self
}
pub fn label(mut self, field: String, label: String) -> Self {
self.config.labels.insert(field, label);
self
}
pub fn help_text(mut self, field: String, text: String) -> Self {
self.config.help_texts.insert(field, text);
self
}
pub fn build(self, instance: Option<T>) -> ModelForm<T> {
ModelForm::new(instance, self.config)
}
}
impl<T: FormModel> Default for ModelFormBuilder<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[derive(Debug)]
struct TestModel {
id: i32,
name: String,
email: String,
}
impl FormModel for TestModel {
fn field_names() -> Vec<String> {
vec!["id".to_string(), "name".to_string(), "email".to_string()]
}
fn field_type(name: &str) -> Option<FieldType> {
match name {
"id" => Some(FieldType::Integer),
"name" => Some(FieldType::Char {
max_length: Some(100),
}),
"email" => Some(FieldType::Email),
_ => None,
}
}
fn get_field(&self, name: &str) -> Option<Value> {
match name {
"id" => Some(Value::Number(self.id.into())),
"name" => Some(Value::String(self.name.clone())),
"email" => Some(Value::String(self.email.clone())),
_ => None,
}
}
fn set_field(&mut self, name: &str, value: Value) -> Result<(), String> {
match name {
"id" => {
if let Value::Number(n) = value {
self.id = n.as_i64().unwrap() as i32;
Ok(())
} else {
Err("Invalid type for id".to_string())
}
}
"name" => {
if let Value::String(s) = value {
self.name = s;
Ok(())
} else {
Err("Invalid type for name".to_string())
}
}
"email" => {
if let Value::String(s) = value {
self.email = s;
Ok(())
} else {
Err("Invalid type for email".to_string())
}
}
_ => Err(format!("Unknown field: {}", name)),
}
}
fn save(&mut self) -> Result<(), String> {
Ok(())
}
}
#[rstest]
fn test_model_form_config() {
let config = ModelFormConfig::new()
.fields(vec!["name".to_string(), "email".to_string()])
.exclude(vec!["id".to_string()]);
assert_eq!(
config.fields,
Some(vec!["name".to_string(), "email".to_string()])
);
assert_eq!(config.exclude, vec!["id".to_string()]);
}
#[rstest]
fn test_model_form_builder() {
let instance = TestModel {
id: 1,
name: "John".to_string(),
email: "john@example.com".to_string(),
};
let form = ModelFormBuilder::<TestModel>::new()
.fields(vec!["name".to_string(), "email".to_string()])
.build(Some(instance));
assert!(form.instance().is_some());
}
#[rstest]
fn test_model_field_names() {
let fields = TestModel::field_names();
assert_eq!(
fields,
vec!["id".to_string(), "name".to_string(), "email".to_string()]
);
}
#[rstest]
fn test_save_without_instance_returns_no_instance_error() {
let config = ModelFormConfig::new();
let mut form = ModelForm::<TestModel>::empty(config);
let result = form.save();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err, FormError::NoInstance),
"Expected FormError::NoInstance, got: {err}"
);
}
}